From 2bb72ac43da0ba51ce94d3156af6b5003fa1468b Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 10 Apr 2026 08:33:58 +0000 Subject: [PATCH] docs: document C interop benchmarks and K/N MUL limitation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Benchmarked two approaches for hardware 128-bit multiply on K/N: 1. Full mulWide via C interop (memScoped + allocArray + fe4_mul_reduce): FieldP.mul: 44ns → 116ns (2.6x SLOWER — copy/marshal overhead) 2. Per-call umulh via C interop (fe4_umulh, 20 calls per field mul): FieldP.mul: 44ns → 331ns (7.5x SLOWER — ~15ns bridge per call) Conclusion: K/N cinterop bridge adds ~15ns per call, making fine-grained C interop unviable for the multiply-high hot path (20+ calls per field op). The pure-Kotlin fused approach (4 IMUL per 128-bit product) remains optimal at 44ns/op until K/N supports hardware MUL natively. Updated FieldMulPlatform.native.kt docs with benchmarked rationale. Fixed remaining LongArray references in native benchmark test. https://claude.ai/code/session_01Sxi6Gpxbstuj3Y8TBY7XrU --- .../secp256k1/Secp256k1NativeBenchmark.kt | 14 ++++++------- .../secp256k1/FieldMulPlatform.native.kt | 20 +++++++++++++++++-- 2 files changed, 25 insertions(+), 9 deletions(-) diff --git a/quartz/src/linuxX64Test/kotlin/com/vitorpamplona/quartz/utils/secp256k1/Secp256k1NativeBenchmark.kt b/quartz/src/linuxX64Test/kotlin/com/vitorpamplona/quartz/utils/secp256k1/Secp256k1NativeBenchmark.kt index f65d2c948..cf7c717d8 100644 --- a/quartz/src/linuxX64Test/kotlin/com/vitorpamplona/quartz/utils/secp256k1/Secp256k1NativeBenchmark.kt +++ b/quartz/src/linuxX64Test/kotlin/com/vitorpamplona/quartz/utils/secp256k1/Secp256k1NativeBenchmark.kt @@ -220,35 +220,35 @@ class Secp256k1NativeBenchmark { // out of the loop (dead code elimination risk with constant inputs). val a = U256.fromBytes(hexToBytes("79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798")) val b = U256.fromBytes(hexToBytes("483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8")) - val out = LongArray(4) - val w = LongArray(8) + val out = Fe4() + val w = Wide8() val results = mutableListOf() // --- FieldP.mul (the hottest operation) --- // Uses `out` as both output and next input to create a data dependency chain. - a.copyInto(out) + out.copyFrom(a) results += bench("FieldP.mul", 5000, 100000) { FieldP.mul(out, out, b, w) } // --- FieldP.sqr --- - a.copyInto(out) + out.copyFrom(a) results += bench("FieldP.sqr", 5000, 100000) { FieldP.sqr(out, out, w) } // --- FieldP.add --- - a.copyInto(out) + out.copyFrom(a) results += bench("FieldP.add", 5000, 500000) { FieldP.add(out, out, b) } // --- FieldP.sub --- - a.copyInto(out) + out.copyFrom(a) results += bench("FieldP.sub", 5000, 500000) { FieldP.sub(out, out, b) @@ -277,7 +277,7 @@ class Secp256k1NativeBenchmark { printResults("secp256k1 Field Micro-Benchmarks: Kotlin/Native (LLVM AOT) on linuxX64", results) // Use sinks to prevent dead code elimination of the entire benchmark - assertTrue(hiSink != Long.MIN_VALUE || ltSink != Long.MIN_VALUE || out[0] != Long.MIN_VALUE) + assertTrue(hiSink != Long.MIN_VALUE || ltSink != Long.MIN_VALUE || out.l0 != Long.MIN_VALUE) } private fun hexToBytes(hex: String): ByteArray { diff --git a/quartz/src/nativeMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/FieldMulPlatform.native.kt b/quartz/src/nativeMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/FieldMulPlatform.native.kt index f98da5602..c24ae42fd 100644 --- a/quartz/src/nativeMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/FieldMulPlatform.native.kt +++ b/quartz/src/nativeMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/FieldMulPlatform.native.kt @@ -21,8 +21,24 @@ package com.vitorpamplona.quartz.utils.secp256k1 /** - * Native (iOS/macOS) fused field multiply/square using pure-Kotlin fallback. - * Kotlin/Native compiles ahead-of-time, so inline + direct call is optimal. + * Native fused field multiply/square using pure-Kotlin fallback. + * + * Uses fieldMulReduceFused which computes both lo and hi from shared 32-bit + * sub-products (4 IMUL per 128-bit product instead of 5). This is the optimal + * approach for Kotlin/Native because: + * + * 1. K/N has no way to emit the hardware MUL instruction (64×64→128) from + * Kotlin code. Long * Long only gives the lower 64 bits (IMUL). + * + * 2. C interop was benchmarked and adds ~15ns per call through the K/N bridge + * layer. With 20 multiply-high calls per field multiply, the bridge overhead + * (~300ns) far exceeds the savings from hardware MUL (~150ns saved). + * + * 3. The fused mulFull approach (FieldMulFused.kt) computes both lo and hi + * from 4 shared sub-products, saving 1 IMUL per product vs separate + * lo (IMUL) + hi (4 IMUL fallback) = 5 IMUL. + * + * Measured: 44 ns/op on linuxX64 (pure Kotlin fused) vs 331 ns/op (C interop). */ internal actual fun fieldMulReduce( out: Fe4,