perf: pre-allocate P-side tables and batch inversion temps in PointScratch

Eliminates ~80 LongArray allocations per mul/mulDoubleG call by
pre-allocating the P-side Jacobian and affine tables, the doubling
temp, and the batch inversion scratch buffers in PointScratch
(thread-local, allocated once per thread, reused across calls).

Before: mul() allocated 8 MutablePoint (24 LongArray) + 8 MutablePoint
(24 LongArray) + 16 AffinePoint (32 LongArray) + batch temps = ~92
LongArray per call. After: 0 allocations in the table construction path.

Also fixes minor allocation in addMixed degenerate case (use t[5]
scratch instead of new LongArray(4)).

https://claude.ai/code/session_01BhU63WUe9AhikZxRdw3Lpg
This commit is contained in:
Claude
2026-04-06 19:22:38 +00:00
parent 09427fe6cf
commit 20bab17440
@@ -332,6 +332,20 @@ internal object ECPoint {
// Pre-allocated scratch for wNAF mixed addition // Pre-allocated scratch for wNAF mixed addition
val mixTmp = MutablePoint() val mixTmp = MutablePoint()
val mixNegY = LongArray(4) val mixNegY = LongArray(4)
// Pre-allocated P-side tables for mul/mulDoubleG (avoids ~80 LongArray allocs per call)
val pOddJac = Array(8) { MutablePoint() }
val pLamOddJac = Array(8) { MutablePoint() }
val pOddAff = Array(8) { AffinePoint() }
val pLamOddAff = Array(8) { AffinePoint() }
val p2 = MutablePoint() // doublePoint temp for table building
// Pre-allocated batch inversion temps (avoids 12 LongArray allocs per call)
val cumZ = Array(8) { LongArray(4) }
val batchInv = LongArray(4)
val batchZInv = LongArray(4)
val batchZInv2 = LongArray(4)
val batchZInv3 = LongArray(4)
} }
private val scratch = ThreadLocal.withInitial { PointScratch() } private val scratch = ThreadLocal.withInitial { PointScratch() }
@@ -429,9 +443,8 @@ internal object ECPoint {
FieldP.sub(t[4], t[2], p.x) // H = U₂ - U₁ FieldP.sub(t[4], t[2], p.x) // H = U₂ - U₁
if (U256.isZero(t[4])) { if (U256.isZero(t[4])) {
val tmp = LongArray(4) FieldP.sub(t[5], t[3], p.y)
FieldP.sub(tmp, t[3], p.y) if (U256.isZero(t[5])) doublePoint(out, p, s) else out.setInfinity()
if (U256.isZero(tmp)) doublePoint(out, p, s) else out.setInfinity()
return return
} }
@@ -558,27 +571,23 @@ internal object ECPoint {
val wnaf2 = s.wnaf2 val wnaf2 = s.wnaf2
// P odd-multiples [1P, 3P, 5P, ..., 15P] via 2P stepping (Jacobian) // P odd-multiples [1P, 3P, 5P, ..., 15P] via 2P stepping (Jacobian)
val p2 = MutablePoint() // Uses pre-allocated tables from PointScratch to avoid ~80 LongArray allocs
doublePoint(p2, p, s) doublePoint(s.p2, p, s)
val pOddJac = Array(tableSize) { MutablePoint() } val pOddJac = s.pOddJac
pOddJac[0].copyFrom(p) pOddJac[0].copyFrom(p)
for (i in 1 until tableSize) addPoints(pOddJac[i], pOddJac[i - 1], p2, s) for (i in 1 until tableSize) addPoints(pOddJac[i], pOddJac[i - 1], s.p2, s)
// λ(P) odd-multiples: (β·X, Y, Z) — Z is identical to pOddJac (endomorphism preserves Z) // λ(P) odd-multiples: (β·X, Y, Z) — Z is identical to pOddJac
val pLamOddJac = val pLamOddJac = s.pLamOddJac
Array(tableSize) { i -> for (i in 0 until tableSize) {
val lp = MutablePoint() FieldP.mul(pLamOddJac[i].x, pOddJac[i].x, Glv.BETA, s.w)
FieldP.mul(lp.x, pOddJac[i].x, Glv.BETA, s.w) pOddJac[i].y.copyInto(pLamOddJac[i].y)
pOddJac[i].y.copyInto(lp.y) pOddJac[i].z.copyInto(pLamOddJac[i].z)
pOddJac[i].z.copyInto(lp.z)
lp
} }
// Effective-affine: batch-convert to affine for cheaper mixed additions. // Effective-affine: batch-convert with shared Z inversion
// Since pLamOddJac has the same Z coordinates as pOddJac, we batch-invert val pOdd = s.pOddAff
// once and reuse the Z⁻¹ values for both tables (saves one full inversion). val pLamOdd = s.pLamOddAff
val pOdd = Array(tableSize) { AffinePoint() }
val pLamOdd = Array(tableSize) { AffinePoint() }
batchToAffinePair(pOddJac, pLamOddJac, pOdd, pLamOdd, s) batchToAffinePair(pOddJac, pLamOddJac, pOdd, pLamOdd, s)
// Find highest non-zero digit // Find highest non-zero digit
@@ -683,25 +692,21 @@ internal object ECPoint {
val gOdd = gOddTable val gOdd = gOddTable
val gLam = gLamTable val gLam = gLamTable
// P odd-multiples [1P, 3P, 5P, ..., 15P] via 2P stepping (1 double + 7 adds) // P odd-multiples [1P, 3P, 5P, ..., 15P] — uses pre-allocated scratch tables
val p2 = MutablePoint() doublePoint(sc.p2, p, sc)
doublePoint(p2, p, sc) val pOddJac = sc.pOddJac
val pOddJac = Array(pTableSize) { MutablePoint() }
pOddJac[0].copyFrom(p) pOddJac[0].copyFrom(p)
for (i in 1 until pTableSize) addPoints(pOddJac[i], pOddJac[i - 1], p2, sc) for (i in 1 until pTableSize) addPoints(pOddJac[i], pOddJac[i - 1], sc.p2, sc)
// λ(P) table: (β·X, Y, Z) in Jacobian — endomorphism preserves projective coords val pLamOddJac = sc.pLamOddJac
val pLamOddJac = for (i in 0 until pTableSize) {
Array(pTableSize) { i -> FieldP.mul(pLamOddJac[i].x, pOddJac[i].x, Glv.BETA, sc.w)
val lp = MutablePoint() pOddJac[i].y.copyInto(pLamOddJac[i].y)
FieldP.mul(lp.x, pOddJac[i].x, Glv.BETA, sc.w) pOddJac[i].z.copyInto(pLamOddJac[i].z)
pOddJac[i].y.copyInto(lp.y)
pOddJac[i].z.copyInto(lp.z)
lp
} }
// Effective-affine: batch-convert P-side tables (shared Z inversion) // Effective-affine: batch-convert P-side tables (shared Z inversion)
val pOdd = Array(pTableSize) { AffinePoint() } val pOdd = sc.pOddAff
val pLamOdd = Array(pTableSize) { AffinePoint() } val pLamOdd = sc.pLamOddAff
batchToAffinePair(pOddJac, pLamOddJac, pOdd, pLamOdd, sc) batchToAffinePair(pOddJac, pLamOddJac, pOdd, pLamOdd, sc)
// Find highest non-zero digit across all 4 streams // Find highest non-zero digit across all 4 streams
@@ -862,20 +867,20 @@ internal object ECPoint {
val w = s.w val w = s.w
// Build prefix products of Z (shared between a and b) // Build prefix products of Z (shared between a and b)
val cumZ = Array(n) { LongArray(4) } val cumZ = s.cumZ
a[0].z.copyInto(cumZ[0]) a[0].z.copyInto(cumZ[0])
for (i in 1 until n) { for (i in 1 until n) {
FieldP.mul(cumZ[i], cumZ[i - 1], a[i].z, w) FieldP.mul(cumZ[i], cumZ[i - 1], a[i].z, w)
} }
// Single inversion of the total product // Single inversion of the total product
val inv = LongArray(4) val inv = s.batchInv
FieldP.inv(inv, cumZ[n - 1]) FieldP.inv(inv, cumZ[n - 1])
// Recover individual Z⁻¹ and convert both tables // Recover individual Z⁻¹ and convert both tables
val zInv = LongArray(4) val zInv = s.batchZInv
val zInv2 = LongArray(4) val zInv2 = s.batchZInv2
val zInv3 = LongArray(4) val zInv3 = s.batchZInv3
for (i in n - 1 downTo 1) { for (i in n - 1 downTo 1) {
FieldP.mul(zInv, inv, cumZ[i - 1], w) FieldP.mul(zInv, inv, cumZ[i - 1], w)