mirror of
https://github.com/luxfi/pulsar.git
synced 2026-09-24 00:25:52 +00:00
bench(pulsar): mlwe-conversion overhead — de-dup regression check
Decomplected, one concern: the per-op poly<->mlwe.Poly marshal the Phase-4 DRY de-dup put into the ring hot loop (toMLWE/fromMLWE). Isolates pure conversion vs ring math: toMLWE, fromMLWE, RoundTrip (pure tax), NTT/INTT/MulNTT via mlwe. VERDICT: de-dup did NOT regress perf where it matters. - Allocations: ZERO. ntt/invNTT/mulHat all report 0 allocs/op. toMLWE in ISOLATION shows 1 alloc/2048B only because the sink forces the result to escape; -gcflags=-m proves toMLWE is inlined at every ring-op call site (mldsa_lattice.go ntt:230, invNTT:244, mulHat:174x2) with 'make([]uint64,256) does not escape' -> stack-allocated. Only toMLWE's own generic return path (line 96) escapes, never the hot loop. - CPU: pure conversion RoundTrip 249ns is ~12% of an NTT (2043ns), ~25% of MulNTT (1507ns) -- a modest constant factor, the price of one audited ring core (luxfi/mlwe), NOT a GC/alloc regression. BRAID FLAGGED (not fixed -- bench task must not refactor production crypto): the conversion is braided into every ring op because luxfi/mlwe's Ring speaks []uint64 while pulsar's wire/FindHint frame needs [256]uint32. A zero-copy mlwe API over borrowed buffers could reclaim the constant factor; out of scope here.
This commit is contained in:
@@ -0,0 +1,137 @@
|
||||
// Copyright (C) 2025-2026, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
// bench_mlwe_conv_test.go — DECOMPLECTED benchmark, ONE concern: the per-op
|
||||
// poly↔mlwe.Poly CONVERSION overhead the Phase-4 DRY de-dup introduced into the
|
||||
// ring hot loop. After routing the FIPS-204 ring core through
|
||||
// github.com/luxfi/mlwe (mldsa_lattice.go / mldsa_sample.go), every ntt/invNTT/
|
||||
// mulHat first widens Pulsar's [256]uint32 poly into an mlwe.Poly{[]uint64}
|
||||
// (toMLWE — which ALLOCATES make([]uint64, 256)) and narrows the result back
|
||||
// (fromMLWE). This benchmark measures whether that marshal is a measurable
|
||||
// regression on the signing hot path, where polyDotHat → mulHat runs per matrix
|
||||
// element and ntt runs per poly.
|
||||
//
|
||||
// The measurement is decomplected into the pure conversion vs the real ring math:
|
||||
//
|
||||
// - toMLWE : widen [256]uint32 → mlwe.Poly (1 heap alloc, 256-coeff copy)
|
||||
// - fromMLWE : narrow mlwe.Poly → [256]uint32 (0 allocs, value return)
|
||||
// - RoundTrip: fromMLWE(p.toMLWE()) — the PURE conversion tax per ring op,
|
||||
// no ring math; this is exactly the cost the de-dup added to each ntt
|
||||
// - NTT : p.ntt() = toMLWE + ring NTT + fromMLWE (conversion + math)
|
||||
// - MulNTT : p.mulHat = 2×toMLWE + ring MulNTT + fromMLWE (conversion + math)
|
||||
//
|
||||
// The de-dup OVERHEAD FRACTION is RoundTrip / NTT: if the conversion is a small
|
||||
// slice of the actual transform, the DRY win cost nothing measurable; if it
|
||||
// dominates, it is flagged. The number is reported honestly in the suite report;
|
||||
// this file does NOT refactor the production conversion (a bench task must not
|
||||
// touch production crypto — any braid is FLAGGED, not fixed).
|
||||
//
|
||||
// Run:
|
||||
//
|
||||
// cd pulsar && export SDKROOT="$(xcrun --show-sdk-path)"; export GOWORK=off
|
||||
// go test -run='^$' -bench='BenchmarkMLWE|BenchmarkRing' -benchmem ./ref/go/pkg/pulsar/
|
||||
package pulsar
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/luxfi/mlwe"
|
||||
)
|
||||
|
||||
// Package-level sinks defeat dead-code elimination so the compiler cannot
|
||||
// optimize the measured op away.
|
||||
var (
|
||||
sinkPoly poly
|
||||
sinkMLWE mlwe.Poly
|
||||
)
|
||||
|
||||
// benchRingPoly builds a deterministic poly with all coefficients in [0, q)
|
||||
// (a valid ring element) via a small LCG — no randomness in the timed path.
|
||||
func benchRingPoly(seed uint32) poly {
|
||||
var p poly
|
||||
x := seed | 1
|
||||
for i := 0; i < mldsaN; i++ {
|
||||
x = x*1664525 + 1013904223
|
||||
p[i] = x % mldsaQ
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
// BenchmarkMLWE_toMLWE isolates the widen [256]uint32 → mlwe.Poly conversion
|
||||
// (the allocating half the de-dup added to every ring op).
|
||||
func BenchmarkMLWE_toMLWE(b *testing.B) {
|
||||
p := benchRingPoly(1)
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
sinkMLWE = p.toMLWE()
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkMLWE_fromMLWE isolates the narrow mlwe.Poly → [256]uint32 conversion.
|
||||
func BenchmarkMLWE_fromMLWE(b *testing.B) {
|
||||
p := benchRingPoly(2)
|
||||
m := p.toMLWE()
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
sinkPoly = fromMLWE(m)
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkMLWE_RoundTrip isolates the PURE conversion tax per ring op:
|
||||
// fromMLWE(toMLWE(p)) with no ring math. This is precisely the overhead the
|
||||
// de-dup added to each ntt/invNTT (and twice over to each mulHat's operands).
|
||||
func BenchmarkMLWE_RoundTrip(b *testing.B) {
|
||||
p := benchRingPoly(3)
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
sinkPoly = fromMLWE(p.toMLWE())
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkRing_NTT_viaMLWE measures one forward NTT through the mlwe path
|
||||
// (toMLWE + ring NTT + fromMLWE). A fresh copy per iteration keeps the input a
|
||||
// valid [0,q) ring element (NTT mutates in place); the [256]uint32 copy is a
|
||||
// stack memmove, no heap alloc.
|
||||
func BenchmarkRing_NTT_viaMLWE(b *testing.B) {
|
||||
base := benchRingPoly(4)
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
q := base
|
||||
q.ntt()
|
||||
sinkPoly = q
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkRing_INTT_viaMLWE measures one inverse NTT through the mlwe path.
|
||||
func BenchmarkRing_INTT_viaMLWE(b *testing.B) {
|
||||
base := benchRingPoly(5)
|
||||
base.ntt() // a well-formed NTT-domain element to invert
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
q := base
|
||||
q.invNTT()
|
||||
sinkPoly = q
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkRing_MulNTT_viaMLWE measures one NTT-domain pointwise product through
|
||||
// the mlwe path (mulHat = 2×toMLWE + ring MulNTT + fromMLWE). The operands are
|
||||
// NTT-domain elements built in setup; mulHat does not mutate them.
|
||||
func BenchmarkRing_MulNTT_viaMLWE(b *testing.B) {
|
||||
aHat := benchRingPoly(6)
|
||||
bHat := benchRingPoly(7)
|
||||
aHat.ntt()
|
||||
bHat.ntt()
|
||||
var p poly
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
p.mulHat(&aHat, &bHat)
|
||||
sinkPoly = p
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user