From 30151295ad1d190bc77aec521ebf0467d97f2b6f Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 8 Apr 2026 23:48:13 +0000 Subject: [PATCH] perf: make uLt() platform-specific to avoid JVM regression MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The inline uLt() using XOR trick in commonMain regressed JVM by ~30% because HotSpot's Long.compareUnsigned is a JIT intrinsic (single unsigned CMP + SETB), while the XOR trick generates 2 extra XOR insns that HotSpot doesn't optimize away. Convert uLt to expect/actual: - JVM: Long.compareUnsigned (HotSpot intrinsic) - Android: XOR trick (avoids ULong.constructor-impl NOOP calls) - Native: XOR trick (no JVM intrinsics available) JVM verify: 2.1x → 1.5x (restored, slightly better than 1.6x baseline) Android: unchanged (still uses XOR trick) https://claude.ai/code/session_01EMY5RnXb9rnsyU2KbXrSaY --- .../secp256k1/UnsignedCompare.android.kt | 32 +++++++++++++++++++ .../quartz/utils/secp256k1/U256.kt | 24 ++++++-------- .../utils/secp256k1/UnsignedCompare.jvm.kt | 31 ++++++++++++++++++ .../utils/secp256k1/UnsignedCompare.native.kt | 30 +++++++++++++++++ 4 files changed, 103 insertions(+), 14 deletions(-) create mode 100644 quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/UnsignedCompare.android.kt create mode 100644 quartz/src/jvmMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/UnsignedCompare.jvm.kt create mode 100644 quartz/src/nativeMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/UnsignedCompare.native.kt diff --git a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/UnsignedCompare.android.kt b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/UnsignedCompare.android.kt new file mode 100644 index 0000000000..ccc87e0975 --- /dev/null +++ b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/UnsignedCompare.android.kt @@ -0,0 +1,32 @@ +/* + * 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 + +/** + * Android: XOR-with-MIN_VALUE trick avoids the ULong.constructor-impl + * NOOP invokestatic calls that Kotlin's toULong() generates. Produces + * pure arithmetic bytecode (lxor, lcmp) with zero method calls. + * ART's JIT compiles this to EOR + CMP + CSET on ARM64. + */ +internal actual fun uLt( + a: Long, + b: Long, +): Boolean = (a xor Long.MIN_VALUE) < (b xor Long.MIN_VALUE) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/U256.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/U256.kt index 8f06c06a18..8b314793f6 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/U256.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/U256.kt @@ -46,26 +46,22 @@ package com.vitorpamplona.quartz.utils.secp256k1 // ===================================================================================== /** - * Unsigned less-than comparison without ULong inline class overhead. + * Unsigned less-than comparison, platform-optimized. * - * Kotlin's `uLt(a, b)` generates 2 invokestatic calls to - * ULong.constructor-impl (NOOPs that return the input unchanged) plus - * Long.compareUnsigned per comparison. On ART, these extra invokestatic - * calls add ~2-3ns each × ~18,000 comparisons per verify = ~36-54μs. + * On JVM (HotSpot): uses Long.compareUnsigned which is a JIT intrinsic, + * compiling to a single unsigned CMP + SETB instruction. * - * This inline function uses the XOR-with-MIN_VALUE trick directly, - * producing pure arithmetic bytecode with ZERO method calls: - * lload a, ldc MIN_VALUE, lxor, lload b, ldc MIN_VALUE, lxor, lcmp, ifge + * On Android (ART): uses XOR-with-MIN_VALUE trick to avoid the + * ULong.constructor-impl NOOP invokestatic calls that Kotlin's toULong() + * generates (~17,800 per verify). Produces pure arithmetic bytecode with + * zero method calls. * - * vs the toULong() path: - * lload a, invokestatic ULong.constructor-impl, lload b, - * invokestatic ULong.constructor-impl, invokestatic Long.compareUnsigned, ifge + * On Native: uses XOR-with-MIN_VALUE (no JVM intrinsics available). */ -@Suppress("NOTHING_TO_INLINE") -internal inline fun uLt( +internal expect fun uLt( a: Long, b: Long, -): Boolean = (a xor Long.MIN_VALUE) < (b xor Long.MIN_VALUE) +): Boolean /** * Raw 256-bit unsigned integer arithmetic using 4×64-bit limbs. diff --git a/quartz/src/jvmMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/UnsignedCompare.jvm.kt b/quartz/src/jvmMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/UnsignedCompare.jvm.kt new file mode 100644 index 0000000000..f67271f094 --- /dev/null +++ b/quartz/src/jvmMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/UnsignedCompare.jvm.kt @@ -0,0 +1,31 @@ +/* + * 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 + +/** + * JVM: Long.compareUnsigned is a HotSpot JIT intrinsic that compiles + * to a single unsigned CMP + SETB instruction. Much faster than the + * XOR trick on HotSpot. + */ +internal actual fun uLt( + a: Long, + b: Long, +): Boolean = java.lang.Long.compareUnsigned(a, b) < 0 diff --git a/quartz/src/nativeMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/UnsignedCompare.native.kt b/quartz/src/nativeMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/UnsignedCompare.native.kt new file mode 100644 index 0000000000..aeb48d96ed --- /dev/null +++ b/quartz/src/nativeMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/UnsignedCompare.native.kt @@ -0,0 +1,30 @@ +/* + * 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 + +/** + * Native: XOR-with-MIN_VALUE trick (no JVM intrinsics available). + * Kotlin/Native AOT compiles this to efficient unsigned compare. + */ +internal actual fun uLt( + a: Long, + b: Long, +): Boolean = (a xor Long.MIN_VALUE) < (b xor Long.MIN_VALUE)