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:
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user