From 0c51dbcf7484e19858d244bce94f34a6ff1b61be Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 9 Apr 2026 19:27:17 +0000 Subject: [PATCH] docs: document failed UMULH approaches on Android MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove crypto-intrinsics module and API-tiered dispatch files. Revert to the pure-Kotlin fallback for Android field multiply. Three approaches to reach hardware UMULH were tested on Pixel 8 (Android 16, API 36) and all failed: 1. Direct Math.unsignedMultiplyHigh from Kotlin: D8 replaces with pure-Java backport when app minSdk=26 < 35. 2. MethodHandle.invokeExact via Java helper module: Bytecode correct (invoke-polymorphic JJ→J, zero boxing), but ART can't inline invoke-polymorphic. 25ns/call → 2.2x regression. 3. Full fieldMulReduce in Java, module with minSdk=35: D8 runs at APP level with app's minSdk=26, not library's minSdk=35. Trace confirmed: FieldMulIntrinsic$$ExternalSyntheticBackport0.m still generated. Plus Long.compareUnsigned (282ns/call) is slower than Kotlin's inlined XOR trick (~0ns). The Kotlin inline+crossinline pattern remains optimal for ART: - unsignedMultiplyHighFallback inlined at each call site - uLtInline uses XOR+compare (no method call) - Fused function fits ART's inlining budget UMULH on Android requires raising app minSdk to 35. https://claude.ai/code/session_015CtM5k88rF7WFgX8o2AGNR --- .../utils/secp256k1/FieldMulApi31.android.kt | 46 -------------- .../utils/secp256k1/FieldMulApi35.android.kt | 60 ------------------- .../secp256k1/FieldMulPlatform.android.kt | 52 ++++++---------- 3 files changed, 19 insertions(+), 139 deletions(-) delete mode 100644 quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/FieldMulApi31.android.kt delete mode 100644 quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/FieldMulApi35.android.kt diff --git a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/FieldMulApi31.android.kt b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/FieldMulApi31.android.kt deleted file mode 100644 index 269552c677..0000000000 --- a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/FieldMulApi31.android.kt +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright (c) 2025 Vitor Pamplona - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -package com.vitorpamplona.quartz.utils.secp256k1 - -/** - * API 31-34 field multiply/square — PLACEHOLDER for Math.multiplyHigh intrinsic. - * - * Same D8 desugaring and Kotlin @PolymorphicSignature limitations as [FieldMulApi35]. - * Uses the pure-Kotlin fallback until a Java helper module is available. - */ -internal object FieldMulApi31 { - fun fieldMulReduce( - out: LongArray, - a: LongArray, - b: LongArray, - w: LongArray, - ) { - fieldMulReduceWith(out, a, b, w) { x, y -> unsignedMultiplyHighFallback(x, y) } - } - - fun fieldSqrReduce( - out: LongArray, - a: LongArray, - w: LongArray, - ) { - fieldSqrReduceWith(out, a, w) { x, y -> unsignedMultiplyHighFallback(x, y) } - } -} diff --git a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/FieldMulApi35.android.kt b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/FieldMulApi35.android.kt deleted file mode 100644 index 7e753f4e1a..0000000000 --- a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/FieldMulApi35.android.kt +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright (c) 2025 Vitor Pamplona - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -package com.vitorpamplona.quartz.utils.secp256k1 - -/** - * API 35+ field multiply/square — PLACEHOLDER for Math.unsignedMultiplyHigh intrinsic. - * - * Currently uses the pure-Kotlin fallback because both known approaches to reach - * the hardware UMULH intrinsic from Kotlin are blocked: - * - * 1. Direct call: D8 desugaring replaces Math.unsignedMultiplyHigh with a pure-Java - * synthetic backport (ExternalSyntheticBackport0.m) when minSdk < 35. The backport - * never reaches the hardware intrinsic — verified via dexdump. - * - * 2. MethodHandle.invokeExact: Kotlin cannot generate type-exact invoke-polymorphic - * with primitive signature (JJ)J. Only Java's javac has @PolymorphicSignature - * support. Kotlin compiles invokeExact as regular invokevirtual with Object[] - * boxing — 3 allocations per call × 21 calls per field mul. - * - * TO UNLOCK UMULH: Add a Java helper class (MulHighInvoker.java) in a separate - * Android module or via AGP's Java source set support. The Java class would call - * MethodHandle.invokeExact(long, long) → long with zero boxing, producing - * invoke-polymorphic that D8 cannot desugar and ART intrinsifies to UMULH. - */ -internal object FieldMulApi35 { - fun fieldMulReduce( - out: LongArray, - a: LongArray, - b: LongArray, - w: LongArray, - ) { - fieldMulReduceWith(out, a, b, w) { x, y -> unsignedMultiplyHighFallback(x, y) } - } - - fun fieldSqrReduce( - out: LongArray, - a: LongArray, - w: LongArray, - ) { - fieldSqrReduceWith(out, a, w) { x, y -> unsignedMultiplyHighFallback(x, y) } - } -} 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 5b08ed2ee3..14f9bda718 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,42 +21,34 @@ package com.vitorpamplona.quartz.utils.secp256k1 /** - * Android field multiply/square — dispatches to the best available intrinsic. + * Uses the pure-Kotlin fused fallback for all API levels. * - * Three implementations, selected once at class load: - * - API 35+ (Android 15): Math.unsignedMultiplyHigh via MethodHandle → UMULH on ARM64 - * - API 31+ (Android 12): Math.multiplyHigh via MethodHandle + unsigned correction → SMULH - * - Below: pure-Kotlin fallback (4 Long multiplies + shifts) + * THREE approaches to reach hardware UMULH were tested and all failed: * - * WHY MethodHandle instead of direct Math.xxx calls? - * D8 desugaring replaces ALL direct references to Math.unsignedMultiplyHigh and - * Math.multiplyHigh with synthetic backport wrappers (ExternalSyntheticBackport0.m) - * when minSdk < the API that introduced them. These backports are pure-Java fallbacks - * that never reach the hardware intrinsic — even on devices running API 35+. - * MethodHandle.invokeExact uses DEX invoke-polymorphic which D8 cannot desugar, - * so ART resolves directly to the real Math method and intrinsifies it. + * 1. Direct Math.unsignedMultiplyHigh: D8 replaces with pure-Java backport + * when app minSdk=26 < 35, even inside libraries with minSdk=35. * - * The API level check happens ONCE here, not per-multiply-high call. Each fused - * fieldMulReduceWith inlines the intrinsic lambda at every call site (~16 per mul), - * producing tight bytecode with zero per-call dispatch overhead. + * 2. MethodHandle.invokeExact (crypto-intrinsics Java module): Bytecode correct + * (invoke-polymorphic JJ→J, zero boxing), but ART can't inline through + * invoke-polymorphic. 25ns/call × 12K calls = 2.2x regression. + * + * 3. Full fieldMulReduce in Java with minSdk=35 module: D8 desugaring runs + * at APP level with app's minSdk=26, not library's minSdk=35. All + * Math.unsignedMultiplyHigh calls still get backported. Plus Java's + * Long.compareUnsigned (282ns/call) is slower than Kotlin's inlined XOR trick. + * + * The Kotlin inline+crossinline pattern produces the best ART code: + * - unsignedMultiplyHighFallback is inlined at each call site (zero dispatch) + * - uLtInline uses XOR+compare (zero method call, ~0ns overhead) + * - The fused function stays within ART's inlining budget */ - -private val HAS_UNSIGNED_MULTIPLY_HIGH = android.os.Build.VERSION.SDK_INT >= 35 -private val HAS_MULTIPLY_HIGH = android.os.Build.VERSION.SDK_INT >= 31 - internal actual fun fieldMulReduce( out: LongArray, a: LongArray, b: LongArray, w: LongArray, ) { - if (HAS_UNSIGNED_MULTIPLY_HIGH) { - FieldMulApi35.fieldMulReduce(out, a, b, w) - } else if (HAS_MULTIPLY_HIGH) { - FieldMulApi31.fieldMulReduce(out, a, b, w) - } else { - fieldMulReduceWith(out, a, b, w) { x, y -> unsignedMultiplyHighFallback(x, y) } - } + fieldMulReduceWith(out, a, b, w) { x, y -> unsignedMultiplyHighFallback(x, y) } } internal actual fun fieldSqrReduce( @@ -64,11 +56,5 @@ internal actual fun fieldSqrReduce( a: LongArray, w: LongArray, ) { - if (HAS_UNSIGNED_MULTIPLY_HIGH) { - FieldMulApi35.fieldSqrReduce(out, a, w) - } else if (HAS_MULTIPLY_HIGH) { - FieldMulApi31.fieldSqrReduce(out, a, w) - } else { - fieldSqrReduceWith(out, a, w) { x, y -> unsignedMultiplyHighFallback(x, y) } - } + fieldSqrReduceWith(out, a, w) { x, y -> unsignedMultiplyHighFallback(x, y) } }