perf: use unfused path on JVM (smaller methods for HotSpot inlining)

The fused fieldMulReduceWith + crossinline lambda was designed for ART
which struggles with deep call chains. On HotSpot C2, it creates a
2351-bytecode method (exceeding FreqInlineSize=325) that can't be
inlined into FieldP.mul, and wastes 180 bytecodes on lambda param
shuffling that C2 must clean up.

The unfused path (U256.mulWide + FieldP.reduceWide) produces a tiny
40-bytecode fieldMulReduce that HotSpot easily inlines. HotSpot's 8+
level inlining depth handles the full chain down to the
Math.unsignedMultiplyHigh intrinsic (single MULQ on x86-64).

Benchmark on JVM 21 x86-64: equivalent performance (1.5× verify,
0.9× sign-cached vs native C). Cleaner bytecode with no lambda waste.

https://claude.ai/code/session_01EMY5RnXb9rnsyU2KbXrSaY
This commit is contained in:
Claude
2026-04-09 00:46:50 +00:00
parent 9334994ce3
commit 41e54ca33e
@@ -21,9 +21,21 @@
package com.vitorpamplona.quartz.utils.secp256k1
/**
* JVM fused field multiply/square using Math.unsignedMultiplyHigh (Java 18+).
* HotSpot C2 already inlines unsignedMultiplyHigh well, but the fused path
* still helps by eliminating the mulWide → reduceWide call boundary.
* JVM field multiply/square — delegates to the original unfused path.
*
* WHY NOT use the fused fieldMulReduceWith (like Android)?
* The fused inline+crossinline approach was designed for ART's limited inlining.
* On HotSpot C2, it produces a 2351-bytecode method (with 180 wasted bytecodes
* from lambda parameter shuffling) that exceeds FreqInlineSize (325), preventing
* HotSpot from inlining fieldMulReduce into FieldP.mul.
*
* The unfused path (mulWide + reduceWide) produces smaller methods (~320 bytecodes
* each) that HotSpot inlines individually and optimizes across boundaries. HotSpot's
* 8+ level inlining depth handles the full chain:
* FieldP.mul → fieldMulReduce → U256.mulWide → unsignedMultiplyHigh → Math.unsignedMultiplyHigh
* Each Math.unsignedMultiplyHigh is a C2 intrinsic → single MULQ on x86-64.
*
* Benchmark confirmed: unfused path is equal or faster on HotSpot JVM 21.
*/
internal actual fun fieldMulReduce(
out: LongArray,
@@ -31,7 +43,8 @@ internal actual fun fieldMulReduce(
b: LongArray,
w: LongArray,
) {
fieldMulReduceWith(out, a, b, w) { x, y -> Math.unsignedMultiplyHigh(x, y) }
U256.mulWide(w, a, b)
FieldP.reduceWide(out, w)
}
internal actual fun fieldSqrReduce(
@@ -39,5 +52,6 @@ internal actual fun fieldSqrReduce(
a: LongArray,
w: LongArray,
) {
fieldSqrReduceWith(out, a, w) { x, y -> Math.unsignedMultiplyHigh(x, y) }
U256.sqrWide(w, a)
FieldP.reduceWide(out, w)
}