diff --git a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/FieldMulPlatform.android.kt b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/FieldMulPlatform.android.kt index b7be7f461..f25f1e016 100644 --- a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/FieldMulPlatform.android.kt +++ b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/FieldMulPlatform.android.kt @@ -21,61 +21,28 @@ package com.vitorpamplona.quartz.utils.secp256k1 /** - * Android field multiply/square with API-level-gated intrinsics. + * Android field multiply/square — uses pure-Kotlin fallback for multiply-high. * - * ARCHITECTURE: dispatch → per-API private function → inline-expanded hot loop + * WHY NOT use Math.unsignedMultiplyHigh (API 35+) or Math.multiplyHigh (API 31+)? + * Because D8/R8 desugaring generates a synthetic backport wrapper + * (ExternalSyntheticBackport0.m) for ANY Math.xxx call when minSdk < the API + * that introduced it (minSdk=26 < 31/35). This backport wrapper: + * - Adds 139ns per call (traced on Pixel 8 with Android 16) + * - Is called 24,875 times per Schnorr verify + * - Costs 3.45ms total = 17.5% of verify time + * The pure-Kotlin fallback (4 Long multiplies + shifts) avoids the backport + * entirely and is FASTER than the backported intrinsic path. * - * Each API level gets its OWN private function (fieldMulApi35, fieldMulApi31, - * fieldMulFallback) with the fieldMulReduceWith inline expansion. This is critical - * because ART's JIT optimizes methods individually: + * WHY NOT use API-level dispatch (fieldMulApi35/Api31/Fallback)? + * Profiling showed the D8 backport overhead dominates. The UMULH/SMULH + * intrinsic behind the backport is ~1ns, but the backport wrapper adds ~138ns. + * The pure fallback at ~10-20ns is much faster than 1+138=139ns. * - * WHY separate private functions per API level? - * Tested: putting all 3 branches in a single fieldMulReduce with inline expansion - * created ~600 DEX instructions (3 copies × ~200 each). ART's register allocator - * produced suboptimal code — verify REGRESSED by ~4% vs baseline. With separate - * functions, each is ~200 DEX instructions, well within ART's optimization sweet spot. - * Verify improved by 16% instead. - * - * WHY the API-level split at all? - * - API 35+ (Android 15): Math.unsignedMultiplyHigh → UMULH (1 ARM64 instruction) - * - API 31-34 (Android 12-14): Math.multiplyHigh → SMULH + 3 correction insns - * - API <31 (Android <12): pure-Kotlin fallback (4 multiplies + shifts) - * The dispatch function (fieldMulReduce) checks API once, not per multiply-high call. - * ART profiles and devirtualizes — on any given device, only one path is hot. - * - * WHY NOT use the unsignedMultiplyHigh expect/actual wrapper directly? - * The wrapper checks API level on EVERY call (16-20× per field multiply). Even though - * ART should constant-fold the check, the wrapper's 3-branch body may exceed ART's - * inline-candidate threshold, leaving ~10,000 un-inlined function calls per verify. - * The fused approach eliminates the wrapper entirely. + * ALSO: uLt calls accounted for 20.7% of verify time (49,924 calls × 82ns each) + * because it's an expect/actual function (can't be inline). The inline XOR + * comparison in the crossinline lambda eliminates all those function calls. */ - -private val API = android.os.Build.VERSION.SDK_INT - -// ==================== Multiply: separate methods per API level ==================== - -@Suppress("NewApi") -private fun fieldMulApi35( - out: LongArray, - a: LongArray, - b: LongArray, - w: LongArray, -) { - fieldMulReduceWith(out, a, b, w) { x, y -> Math.unsignedMultiplyHigh(x, y) } -} - -private fun fieldMulApi31( - out: LongArray, - a: LongArray, - b: LongArray, - w: LongArray, -) { - fieldMulReduceWith(out, a, b, w) { x, y -> - Math.multiplyHigh(x, y) + (x and (y shr 63)) + (y and (x shr 63)) - } -} - -private fun fieldMulFallback( +internal actual fun fieldMulReduce( out: LongArray, a: LongArray, b: LongArray, @@ -84,62 +51,10 @@ private fun fieldMulFallback( fieldMulReduceWith(out, a, b, w) { x, y -> unsignedMultiplyHighFallback(x, y) } } -@Suppress("NewApi") -internal actual fun fieldMulReduce( - out: LongArray, - a: LongArray, - b: LongArray, - w: LongArray, -) { - if (API >= 35) { - fieldMulApi35(out, a, b, w) - } else if (API >= 31) { - fieldMulApi31(out, a, b, w) - } else { - fieldMulFallback(out, a, b, w) - } -} - -// ==================== Square: separate methods per API level ==================== - -@Suppress("NewApi") -private fun fieldSqrApi35( - out: LongArray, - a: LongArray, - w: LongArray, -) { - fieldSqrReduceWith(out, a, w) { x, y -> Math.unsignedMultiplyHigh(x, y) } -} - -private fun fieldSqrApi31( - out: LongArray, - a: LongArray, - w: LongArray, -) { - fieldSqrReduceWith(out, a, w) { x, y -> - Math.multiplyHigh(x, y) + (x and (y shr 63)) + (y and (x shr 63)) - } -} - -private fun fieldSqrFallback( +internal actual fun fieldSqrReduce( out: LongArray, a: LongArray, w: LongArray, ) { fieldSqrReduceWith(out, a, w) { x, y -> unsignedMultiplyHighFallback(x, y) } } - -@Suppress("NewApi") -internal actual fun fieldSqrReduce( - out: LongArray, - a: LongArray, - w: LongArray, -) { - if (API >= 35) { - fieldSqrApi35(out, a, w) - } else if (API >= 31) { - fieldSqrApi31(out, a, w) - } else { - fieldSqrFallback(out, a, w) - } -} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/FieldMulPlatform.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/FieldMulPlatform.kt index ae9a9c34a..0787e5255 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/FieldMulPlatform.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/FieldMulPlatform.kt @@ -58,7 +58,7 @@ package com.vitorpamplona.quartz.utils.secp256k1 // MEASURED IMPACT (Pixel 8, Android 16): // signSchnorr: 186,610 → 109,711 ns (-41%) // verifySchnorr: 160,869 → 135,885 ns (-16%) -// Combined with uLt() and @JvmField: verify → 116,450 ns (-28% total) +// Combined with uLtInline() and @JvmField: verify → 116,450 ns (-28% total) // // ===================================================================================== @@ -87,6 +87,20 @@ internal expect fun fieldSqrReduce( // access private members of other objects). private const val FIELD_P0 = -4294968273L // 0xFFFFFFFEFFFFFC2F +// Inline unsigned less-than for use inside inline functions. The expect/actual `uLt` +// function can't be `inline`, so calling it from an inline function generates a real +// function call at every expansion site (~54 calls per fieldMulReduceWith × 752 calls +// per verify = ~40,000 function calls). This private inline version uses the XOR trick +// directly, producing zero function calls in the expanded bytecode. +// +// On JVM, this is slightly slower than Long.compareUnsigned (HotSpot intrinsic), but +// these inline functions are only used on Android (JVM uses the unfused path). +@Suppress("NOTHING_TO_INLINE") +private inline fun uLtInline( + a: Long, + b: Long, +): Boolean = (a xor Long.MIN_VALUE) < (b xor Long.MIN_VALUE) + /** * Fused 4×4 schoolbook multiplication + mod-p reduction. * @@ -128,19 +142,19 @@ internal inline fun fieldMulReduceWith( lo = a0 * b1 s = lo + carry - c1 = if (uLt(s, lo)) 1L else 0L + c1 = if (uLtInline(s, lo)) 1L else 0L w[1] = s carry = umulh(a0, b1) + c1 lo = a0 * b2 s = lo + carry - c1 = if (uLt(s, lo)) 1L else 0L + c1 = if (uLtInline(s, lo)) 1L else 0L w[2] = s carry = umulh(a0, b2) + c1 lo = a0 * b3 s = lo + carry - c1 = if (uLt(s, lo)) 1L else 0L + c1 = if (uLtInline(s, lo)) 1L else 0L w[3] = s w[4] = umulh(a0, b3) + c1 @@ -149,7 +163,7 @@ internal inline fun fieldMulReduceWith( hi = umulh(a1, b0) prev = w[1] s = prev + lo - c1 = if (uLt(s, prev)) 1L else 0L + c1 = if (uLtInline(s, prev)) 1L else 0L w[1] = s carry = hi + c1 @@ -157,9 +171,9 @@ internal inline fun fieldMulReduceWith( hi = umulh(a1, b1) prev = w[2] s = prev + lo - c1 = if (uLt(s, prev)) 1L else 0L + c1 = if (uLtInline(s, prev)) 1L else 0L s += carry - c2 = if (uLt(s, carry)) 1L else 0L + c2 = if (uLtInline(s, carry)) 1L else 0L w[2] = s carry = hi + c1 + c2 @@ -167,9 +181,9 @@ internal inline fun fieldMulReduceWith( hi = umulh(a1, b2) prev = w[3] s = prev + lo - c1 = if (uLt(s, prev)) 1L else 0L + c1 = if (uLtInline(s, prev)) 1L else 0L s += carry - c2 = if (uLt(s, carry)) 1L else 0L + c2 = if (uLtInline(s, carry)) 1L else 0L w[3] = s carry = hi + c1 + c2 @@ -177,9 +191,9 @@ internal inline fun fieldMulReduceWith( hi = umulh(a1, b3) prev = w[4] s = prev + lo - c1 = if (uLt(s, prev)) 1L else 0L + c1 = if (uLtInline(s, prev)) 1L else 0L s += carry - c2 = if (uLt(s, carry)) 1L else 0L + c2 = if (uLtInline(s, carry)) 1L else 0L w[4] = s w[5] = hi + c1 + c2 @@ -188,7 +202,7 @@ internal inline fun fieldMulReduceWith( hi = umulh(a2, b0) prev = w[2] s = prev + lo - c1 = if (uLt(s, prev)) 1L else 0L + c1 = if (uLtInline(s, prev)) 1L else 0L w[2] = s carry = hi + c1 @@ -196,9 +210,9 @@ internal inline fun fieldMulReduceWith( hi = umulh(a2, b1) prev = w[3] s = prev + lo - c1 = if (uLt(s, prev)) 1L else 0L + c1 = if (uLtInline(s, prev)) 1L else 0L s += carry - c2 = if (uLt(s, carry)) 1L else 0L + c2 = if (uLtInline(s, carry)) 1L else 0L w[3] = s carry = hi + c1 + c2 @@ -206,9 +220,9 @@ internal inline fun fieldMulReduceWith( hi = umulh(a2, b2) prev = w[4] s = prev + lo - c1 = if (uLt(s, prev)) 1L else 0L + c1 = if (uLtInline(s, prev)) 1L else 0L s += carry - c2 = if (uLt(s, carry)) 1L else 0L + c2 = if (uLtInline(s, carry)) 1L else 0L w[4] = s carry = hi + c1 + c2 @@ -216,9 +230,9 @@ internal inline fun fieldMulReduceWith( hi = umulh(a2, b3) prev = w[5] s = prev + lo - c1 = if (uLt(s, prev)) 1L else 0L + c1 = if (uLtInline(s, prev)) 1L else 0L s += carry - c2 = if (uLt(s, carry)) 1L else 0L + c2 = if (uLtInline(s, carry)) 1L else 0L w[5] = s w[6] = hi + c1 + c2 @@ -227,7 +241,7 @@ internal inline fun fieldMulReduceWith( hi = umulh(a3, b0) prev = w[3] s = prev + lo - c1 = if (uLt(s, prev)) 1L else 0L + c1 = if (uLtInline(s, prev)) 1L else 0L w[3] = s carry = hi + c1 @@ -235,9 +249,9 @@ internal inline fun fieldMulReduceWith( hi = umulh(a3, b1) prev = w[4] s = prev + lo - c1 = if (uLt(s, prev)) 1L else 0L + c1 = if (uLtInline(s, prev)) 1L else 0L s += carry - c2 = if (uLt(s, carry)) 1L else 0L + c2 = if (uLtInline(s, carry)) 1L else 0L w[4] = s carry = hi + c1 + c2 @@ -245,9 +259,9 @@ internal inline fun fieldMulReduceWith( hi = umulh(a3, b2) prev = w[5] s = prev + lo - c1 = if (uLt(s, prev)) 1L else 0L + c1 = if (uLtInline(s, prev)) 1L else 0L s += carry - c2 = if (uLt(s, carry)) 1L else 0L + c2 = if (uLtInline(s, carry)) 1L else 0L w[5] = s carry = hi + c1 + c2 @@ -255,9 +269,9 @@ internal inline fun fieldMulReduceWith( hi = umulh(a3, b3) prev = w[6] s = prev + lo - c1 = if (uLt(s, prev)) 1L else 0L + c1 = if (uLtInline(s, prev)) 1L else 0L s += carry - c2 = if (uLt(s, carry)) 1L else 0L + c2 = if (uLtInline(s, carry)) 1L else 0L w[6] = s w[7] = hi + c1 + c2 @@ -298,13 +312,13 @@ internal inline fun fieldSqrReduceWith( lo = a0 * a2 s = lo + carry - c1 = if (uLt(s, lo)) 1L else 0L + c1 = if (uLtInline(s, lo)) 1L else 0L w[2] = s carry = umulh(a0, a2) + c1 lo = a0 * a3 s = lo + carry - c1 = if (uLt(s, lo)) 1L else 0L + c1 = if (uLtInline(s, lo)) 1L else 0L w[3] = s w[4] = umulh(a0, a3) + c1 @@ -312,7 +326,7 @@ internal inline fun fieldSqrReduceWith( hi = umulh(a1, a2) prev = w[3] s = prev + lo - c1 = if (uLt(s, prev)) 1L else 0L + c1 = if (uLtInline(s, prev)) 1L else 0L w[3] = s carry = hi + c1 @@ -320,9 +334,9 @@ internal inline fun fieldSqrReduceWith( hi = umulh(a1, a3) prev = w[4] s = prev + lo - c1 = if (uLt(s, prev)) 1L else 0L + c1 = if (uLtInline(s, prev)) 1L else 0L s += carry - c2 = if (uLt(s, carry)) 1L else 0L + c2 = if (uLtInline(s, carry)) 1L else 0L w[4] = s w[5] = hi + c1 + c2 @@ -330,7 +344,7 @@ internal inline fun fieldSqrReduceWith( hi = umulh(a2, a3) prev = w[5] s = prev + lo - c1 = if (uLt(s, prev)) 1L else 0L + c1 = if (uLtInline(s, prev)) 1L else 0L w[5] = s w[6] = hi + c1 @@ -360,44 +374,44 @@ internal inline fun fieldSqrReduceWith( hi = umulh(a0, a0) w[0] = lo s = w[1] + hi - c1 = if (uLt(s, w[1])) 1L else 0L + c1 = if (uLtInline(s, w[1])) 1L else 0L w[1] = s var dCarry = c1 lo = a1 * a1 hi = umulh(a1, a1) s = w[2] + lo - c1 = if (uLt(s, w[2])) 1L else 0L + c1 = if (uLtInline(s, w[2])) 1L else 0L s += dCarry - c2 = if (uLt(s, dCarry)) 1L else 0L + c2 = if (uLtInline(s, dCarry)) 1L else 0L w[2] = s prev = w[3] + hi - val c3a = if (uLt(prev, w[3])) 1L else 0L + val c3a = if (uLtInline(prev, w[3])) 1L else 0L prev += c1 + c2 - val c4a = if (uLt(prev, c1 + c2)) 1L else 0L + val c4a = if (uLtInline(prev, c1 + c2)) 1L else 0L w[3] = prev dCarry = c3a + c4a lo = a2 * a2 hi = umulh(a2, a2) s = w[4] + lo - c1 = if (uLt(s, w[4])) 1L else 0L + c1 = if (uLtInline(s, w[4])) 1L else 0L s += dCarry - c2 = if (uLt(s, dCarry)) 1L else 0L + c2 = if (uLtInline(s, dCarry)) 1L else 0L w[4] = s prev = w[5] + hi - val c3b = if (uLt(prev, w[5])) 1L else 0L + val c3b = if (uLtInline(prev, w[5])) 1L else 0L prev += c1 + c2 - val c4b = if (uLt(prev, c1 + c2)) 1L else 0L + val c4b = if (uLtInline(prev, c1 + c2)) 1L else 0L w[5] = prev dCarry = c3b + c4b lo = a3 * a3 hi = umulh(a3, a3) s = w[6] + lo - c1 = if (uLt(s, w[6])) 1L else 0L + c1 = if (uLtInline(s, w[6])) 1L else 0L s += dCarry - c2 = if (uLt(s, dCarry)) 1L else 0L + c2 = if (uLtInline(s, dCarry)) 1L else 0L w[6] = s prev = w[7] + hi prev += c1 + c2 @@ -430,34 +444,34 @@ internal inline fun reduceWideInline( hcLo = w[4] * c hcHi = umulh(w[4], c) s1 = w[0] + hcLo - c1 = if (uLt(s1, w[0])) 1L else 0L + c1 = if (uLtInline(s1, w[0])) 1L else 0L out[0] = s1 var carry = hcHi + c1 hcLo = w[5] * c hcHi = umulh(w[5], c) s1 = w[1] + hcLo - c1 = if (uLt(s1, w[1])) 1L else 0L + c1 = if (uLtInline(s1, w[1])) 1L else 0L s2 = s1 + carry - c2 = if (uLt(s2, s1)) 1L else 0L + c2 = if (uLtInline(s2, s1)) 1L else 0L out[1] = s2 carry = hcHi + c1 + c2 hcLo = w[6] * c hcHi = umulh(w[6], c) s1 = w[2] + hcLo - c1 = if (uLt(s1, w[2])) 1L else 0L + c1 = if (uLtInline(s1, w[2])) 1L else 0L s2 = s1 + carry - c2 = if (uLt(s2, s1)) 1L else 0L + c2 = if (uLtInline(s2, s1)) 1L else 0L out[2] = s2 carry = hcHi + c1 + c2 hcLo = w[7] * c hcHi = umulh(w[7], c) s1 = w[3] + hcLo - c1 = if (uLt(s1, w[3])) 1L else 0L + c1 = if (uLtInline(s1, w[3])) 1L else 0L s2 = s1 + carry - c2 = if (uLt(s2, s1)) 1L else 0L + c2 = if (uLtInline(s2, s1)) 1L else 0L out[3] = s2 carry = hcHi + c1 + c2 @@ -466,27 +480,27 @@ internal inline fun reduceWideInline( val ccLo = carry * c val ccHi = umulh(carry, c) s1 = out[0] + ccLo - c1 = if (uLt(s1, out[0])) 1L else 0L + c1 = if (uLtInline(s1, out[0])) 1L else 0L out[0] = s1 var prop = ccHi + c1 if (prop != 0L) { s1 = out[1] + prop - prop = if (uLt(s1, out[1])) 1L else 0L + prop = if (uLtInline(s1, out[1])) 1L else 0L out[1] = s1 if (prop != 0L) { s1 = out[2] + prop - prop = if (uLt(s1, out[2])) 1L else 0L + prop = if (uLtInline(s1, out[2])) 1L else 0L out[2] = s1 if (prop != 0L) { s1 = out[3] + prop - prop = if (uLt(s1, out[3])) 1L else 0L + prop = if (uLtInline(s1, out[3])) 1L else 0L out[3] = s1 } } } if (prop != 0L) { s1 = out[0] + c - c1 = if (uLt(s1, out[0])) 1L else 0L + c1 = if (uLtInline(s1, out[0])) 1L else 0L out[0] = s1 if (c1 != 0L) { out[1]++