Files
dex/test/benchmark/orderbook_bench_test.go
zeekay 7a0fcb06ee DEX Go: BatchVerifyOrders Metal + AMM xyk Metal + Metal anchor
- pkg/lx/signed_order{,_cpu,_gpu,_nogpu}.go: SignedOrder with SigningHash
  and BatchVerifyOrders cgo bridge
- pkg/lx/signed_order_metal_anchor_darwin.{c,go}: keep linker from
  dropping libsecp256k1_metal.a from the static archive
- pkg/lx/amm{,_gpu,_nogpu}.go: ReservePair, ConstantProductOut,
  BatchEvalConstantProduct via Metal kernel
- pkg/lx/{signed_order,amm_gpu}_test.go: tests
- pkg/lx/orderbook.go, test/benchmark/orderbook_bench_test.go: misc

End-to-end through cgo (Apple M1 Max):
  BatchVerifyOrders CPU: 27.91 verifies/sec
  BatchVerifyOrders Metal: 87,229 verifies/sec
2026-05-05 10:17:20 -07:00

193 lines
5.2 KiB
Go

package benchmark
import (
"fmt"
"testing"
"github.com/luxfi/crypto"
"github.com/luxfi/dex/pkg/lx"
"github.com/luxfi/geth/common"
)
// mustSignOrder builds a SignedOrder with a fresh secp256k1 keypair, signs
// the SigningHash digest, and stamps the derived address as Sender.
// Used by BenchmarkBatchVerifyOrders.
func mustSignOrder(b testing.TB, id uint64) lx.SignedOrder {
b.Helper()
key, err := crypto.GenerateKey()
if err != nil {
b.Fatalf("generate key: %v", err)
}
pub := crypto.FromECDSAPub(&key.PublicKey)
var addr common.Address
copy(addr[:], crypto.Keccak256(pub[1:])[12:])
so := lx.SignedOrder{
Order: lx.Order{
ID: id,
Symbol: "BTC-USD",
Type: lx.Limit,
Side: lx.Buy,
Price: 100 + float64(id%100)/10,
Size: 1 + float64(id%5),
ClientID: fmt.Sprintf("c-%d", id),
},
Sender: addr,
}
hash, err := so.SigningHash()
if err != nil {
b.Fatalf("signing hash: %v", err)
}
sig, err := crypto.Sign(hash[:], key)
if err != nil {
b.Fatalf("sign: %v", err)
}
copy(so.Sig[:], sig)
return so
}
// BenchmarkOrderBook benchmarks order processing
func BenchmarkOrderBook(b *testing.B) {
ob := lx.NewOrderBook("BENCH-USD")
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
order := &lx.Order{
ID: uint64(i + 1),
Symbol: "BENCH-USD",
Type: lx.Limit,
Side: lx.Side(i % 2),
Price: 100 + float64(i%100)/10,
Size: 1,
UserID: "bench",
}
ob.AddOrder(order)
}
ordersPerSecond := float64(b.N) / b.Elapsed().Seconds()
b.ReportMetric(ordersPerSecond, "orders/sec")
b.ReportMetric(float64(b.Elapsed().Nanoseconds())/float64(b.N), "ns/order")
}
// BenchmarkOrderBookParallel benchmarks parallel order processing
func BenchmarkOrderBookParallel(b *testing.B) {
ob := lx.NewOrderBook("PARALLEL-USD")
b.ResetTimer()
b.ReportAllocs()
b.RunParallel(func(pb *testing.PB) {
i := 0
for pb.Next() {
order := &lx.Order{
ID: uint64(i + 1),
Symbol: "PARALLEL-USD",
Type: lx.Limit,
Side: lx.Side(i % 2),
Price: 100 + float64(i%100),
Size: 1,
UserID: fmt.Sprintf("user_%d", i%10),
}
ob.AddOrder(order)
i++
}
})
ordersPerSecond := float64(b.N) / b.Elapsed().Seconds()
b.ReportMetric(ordersPerSecond, "orders/sec")
b.ReportMetric(float64(b.Elapsed().Nanoseconds())/float64(b.N), "ns/order")
}
// BenchmarkBatchVerifyOrders compares per-order CPU ecrecover against the
// GPU-batched path on 10k orders. The CPU side is the same per-order oracle
// the test uses as a reference; the "GPU" side calls the batch entry point
// (luxcpp/crypto cgo dispatch when cgo is enabled, otherwise the CPU oracle).
func BenchmarkBatchVerifyOrders(b *testing.B) {
const N = 10_000
// Build the corpus once outside the timer — signing 10k orders with
// fresh keys takes seconds and would dominate every iteration.
orders := make([]lx.SignedOrder, N)
for i := 0; i < N; i++ {
orders[i] = mustSignOrder(b, uint64(i+1))
}
b.Run("CPU_PerOrder", func(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
// Single-element batches → forces per-order dispatch.
for j := range orders {
if _, err := lx.BatchVerifyOrders(orders[j : j+1]); err != nil {
b.Fatal(err)
}
}
}
b.ReportMetric(float64(b.N*N)/b.Elapsed().Seconds(), "verifies/sec")
})
b.Run("GPU_Batch", func(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
if _, err := lx.BatchVerifyOrders(orders); err != nil {
b.Fatal(err)
}
}
b.ReportMetric(float64(b.N*N)/b.Elapsed().Seconds(), "verifies/sec")
})
}
// BenchmarkBatchAMM compares a Go for-loop CPU evaluation against the
// GPU-batched amm_xyk_batch_metal dispatch on 100k tuples.
func BenchmarkBatchAMM(b *testing.B) {
const N = 100_000
reserves := make([]lx.ReservePair, N)
amounts := make([]uint64, N)
for i := 0; i < N; i++ {
reserves[i] = lx.ReservePair{
ReserveX: uint64(1+i) * 1_000_000,
ReserveY: uint64(2+i) * 500_000,
}
amounts[i] = uint64(1 + i%10_000)
}
b.Run("CPU_Loop", func(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
if _, err := lx.BatchEvalConstantProductCPU(reserves, amounts); err != nil {
b.Fatal(err)
}
}
b.ReportMetric(float64(b.N*N)/b.Elapsed().Seconds(), "evals/sec")
})
b.Run("GPU_Batch", func(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
if _, err := lx.BatchEvalConstantProduct(reserves, amounts); err != nil {
b.Fatal(err)
}
}
b.ReportMetric(float64(b.N*N)/b.Elapsed().Seconds(), "evals/sec")
})
}
// LP-108: BenchmarkMLXEngine and BenchmarkPlanetScale removed.
//
// BenchmarkMLXEngine pretended to measure an MLX-accelerated engine
// but called the standard CPU OrderBook and reported a hardcoded
// `b.ReportMetric(597, "ns/order")` literal — a fabricated number,
// not a measurement. The "MLX engine" itself
// (pkg/engine/mlx_engine.go) was a `time.Sleep` stub also archived
// under archive/lp108-2026-05-04/.
//
// BenchmarkPlanetScale reported entirely made-up metrics (markets=5M,
// orders/sec=150M, ns/order=597, watts=370, orders/watt=405405) that
// were never measured by any code in this repo.
//
// Honest CPU benchmarks remain at BenchmarkOrderBook /
// BenchmarkOrderBookParallel above. Real C++ numbers are at
// luxcpp/dex/build/luxdex_bench (5.51 M orders/sec single-symbol).