perf: cache lambda(G) table, optimize P table build, reduce allocations

Three targeted optimizations in the verify hot path:

1. Precompute and cache lambda(G) table: the 8 AffinePoints for λ(G)
   odd-multiples are now lazily initialized once (like gTable) instead
   of recomputing 8 field multiplications per verify call.

2. Efficient P odd-multiples table: build [1P, 3P, 5P, ..., 15P] via
   1 doubling + 7 additions (compute 2P then add repeatedly) instead
   of building all 16 multiples [1P..16P] with 15 additions and
   discarding the even ones.

3. Pre-allocated Jacobian negation scratch: the MutablePoint used for
   negating P-side table entries (when wNAF digit is negative) is now
   allocated once before the main loop instead of per-digit.

Also removed the unused maybeNegateTable function.

Benchmark: verifySchnorr 3,429 → 3,556 ops/s (7.2x vs native)

https://claude.ai/code/session_01BhU63WUe9AhikZxRdw3Lpg
This commit is contained in:
Claude
2026-04-05 18:51:45 +00:00
parent 512ca0fec0
commit 1f885accb1
@@ -163,6 +163,11 @@ internal object ECPoint {
*/ */
internal val gTable: Array<AffinePoint> by lazy { buildGTable() } internal val gTable: Array<AffinePoint> by lazy { buildGTable() }
/** Precomputed λ(G) odd-multiples for GLV: gLamTable[i] = λ((2i+1)·G) as affine. */
private val gLamTable: Array<AffinePoint> by lazy {
Array(8) { AffinePoint(FieldP.mul(gTable[it * 2].x, BETA), gTable[it * 2].y.copyOf()) }
}
private fun buildGTable(): Array<AffinePoint> { private fun buildGTable(): Array<AffinePoint> {
val jac = Array(16) { MutablePoint() } val jac = Array(16) { MutablePoint() }
jac[0].setAffine(GX, GY) jac[0].setAffine(GX, GY)
@@ -682,24 +687,17 @@ internal object ECPoint {
val wnafE1 = wnaf(eSplit.k1, w, 129) val wnafE1 = wnaf(eSplit.k1, w, 129)
val wnafE2 = wnaf(eSplit.k2, w, 129) val wnafE2 = wnaf(eSplit.k2, w, 129)
// G odd-multiples [1G, 3G, 5G, ..., 15G] (affine, from precomputed table) // G tables: precomputed and cached (no per-verify allocation)
val gOddBase = Array(tableSize) { gTable[it * 2] } val gOdd = Array(tableSize) { gTable[it * 2] }
// λ(G) odd-multiples: apply endomorphism (β·x, y) to each G table entry val gLam = gLamTable
val gLamBase = Array(tableSize) { AffinePoint(FieldP.mul(gOddBase[it].x, BETA), gOddBase[it].y.copyOf()) }
// Apply GLV sign: if the split negated k₁/k₂, negate the corresponding table y-coords // P odd-multiples [1P, 3P, 5P, ..., 15P] via 2P stepping (1 double + 7 adds)
// GLV sign is NOT baked into tables. Instead, we XOR the negation flag val p2 = MutablePoint()
// with each wNAF digit's sign in the main loop. This avoids the doublePoint(p2, p)
// double-negation bug where a negative wNAF digit + negated table cancel out. val pOdd = Array(tableSize) { MutablePoint() }
val gOdd = gOddBase pOdd[0].copyFrom(p)
val gLam = gLamBase for (i in 1 until tableSize) addPoints(pOdd[i], pOdd[i - 1], p2)
// λ(P) table: (β·X, Y, Z) in Jacobian — endomorphism preserves projective coords
// P odd-multiples [1P, 3P, ..., 15P] as Jacobian (avoids expensive inversions)
val pAll = Array(16) { MutablePoint() }
pAll[0].copyFrom(p)
for (i in 1 until 16) addPoints(pAll[i], pAll[i - 1], pAll[0])
val pOdd = Array(tableSize) { pAll[it * 2] }
// λ(P) table: (β·X, Y, Z) in Jacobian — endomorphism works in projective coords
val pLamOdd = val pLamOdd =
Array(tableSize) { i -> Array(tableSize) { i ->
val lp = MutablePoint() val lp = MutablePoint()
@@ -721,6 +719,7 @@ internal object ECPoint {
out.setInfinity() out.setInfinity()
val tmp = MutablePoint() val tmp = MutablePoint()
val negY = IntArray(8) val negY = IntArray(8)
val negJac = MutablePoint() // Reused scratch for Jacobian negation
for (i in bits - 1 downTo 0) { for (i in bits - 1 downTo 0) {
doublePoint(out, out) doublePoint(out, out)
@@ -728,19 +727,8 @@ internal object ECPoint {
addWnafMixed(out, tmp, negY, wnafS1, i, gOdd, sSplit.negK1) addWnafMixed(out, tmp, negY, wnafS1, i, gOdd, sSplit.negK1)
addWnafMixed(out, tmp, negY, wnafS2, i, gLam, sSplit.negK2) addWnafMixed(out, tmp, negY, wnafS2, i, gLam, sSplit.negK2)
// Streams 3-4: P-side (Jacobian tables, full addition) // Streams 3-4: P-side (Jacobian tables, full addition)
addWnafJacobian(out, tmp, wnafE1, i, pOdd, eSplit.negK1) addWnafJacobian(out, tmp, negJac, wnafE1, i, pOdd, eSplit.negK1)
addWnafJacobian(out, tmp, wnafE2, i, pLamOdd, eSplit.negK2) addWnafJacobian(out, tmp, negJac, wnafE2, i, pLamOdd, eSplit.negK2)
}
}
/** Conditionally negate all y-coordinates in an affine table. */
private fun maybeNegateTable(
table: Array<AffinePoint>,
negate: Boolean,
): Array<AffinePoint> {
if (!negate) return table
return Array(table.size) { i ->
AffinePoint(table[i].x.copyOf(), FieldP.neg(table[i].y))
} }
} }
@@ -776,6 +764,7 @@ internal object ECPoint {
private fun addWnafJacobian( private fun addWnafJacobian(
out: MutablePoint, out: MutablePoint,
tmp: MutablePoint, tmp: MutablePoint,
negScratch: MutablePoint,
wnafDigits: IntArray, wnafDigits: IntArray,
bitIndex: Int, bitIndex: Int,
table: Array<MutablePoint>, table: Array<MutablePoint>,
@@ -789,12 +778,11 @@ internal object ECPoint {
if (!effectiveNeg) { if (!effectiveNeg) {
addPoints(tmp, out, table[idx]) addPoints(tmp, out, table[idx])
} else { } else {
// Negate the Jacobian point: (X, -Y, Z) // Negate the Jacobian point: (X, -Y, Z) using pre-allocated scratch
val neg = MutablePoint() table[idx].x.copyInto(negScratch.x)
table[idx].x.copyInto(neg.x) FieldP.neg(negScratch.y, table[idx].y)
FieldP.neg(neg.y, table[idx].y) table[idx].z.copyInto(negScratch.z)
table[idx].z.copyInto(neg.z) addPoints(tmp, out, negScratch)
addPoints(tmp, out, neg)
} }
out.copyFrom(tmp) out.copyFrom(tmp)
} }