perf(quartz): hoist lookup tables in Hex.decode/encode/isEqual/readLong; bench new codecs
javap showed the hexToByte/byteToHex field re-loaded on every use inside these methods (16 times per readLong call) — the JVM/ART doesn't reliably prove the load loop-invariant. Hoisting it into a local measured ~25% faster for decode and ~10% for isEqual and readLong on the JVM (4096 random 32-byte ids, best-of-150 rounds, 3 repeats); encode was neutral on HotSpot but is hoisted too since ART is historically worse at this (see the internalIsHex comment). Branchless variants of isHex/isHex64 were also measured and were a wash-to-slightly-worse than the branchy early-exit versions on valid input, so those keep their current implementations. Also adds the new exact-size codecs to the on-device HexBenchmark (decode64, decode64OrNull, encode64, decode128, encode128, toLong256, and the old isHex64+decode two-pass for comparison) so ART numbers can be collected with the existing benchmark harness. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Hwv6XwT9mwGUQc57zH4ky4
This commit is contained in:
@@ -39,9 +39,13 @@ class HexBenchmark {
|
||||
@get:Rule val r = BenchmarkRule()
|
||||
|
||||
val hex = "48a72b485d38338627ec9d427583551f9af4f016c739b8ec0d6313540a8b12cf"
|
||||
val hex128 = hex + "b0635d6a9851d3aed0cd6c495b282167acf761729078d975fc341b22650b07b9"
|
||||
val bytes =
|
||||
fr.acinq.secp256k1.Hex
|
||||
.decode(hex)
|
||||
val bytes64 =
|
||||
fr.acinq.secp256k1.Hex
|
||||
.decode(hex128)
|
||||
|
||||
@Test
|
||||
fun hexIsEqual() {
|
||||
@@ -103,4 +107,40 @@ class HexBenchmark {
|
||||
fun isHex64() {
|
||||
r.measureRepeated { Hex.isHex64(hex) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun hexDecode64() {
|
||||
r.measureRepeated { Hex.decode64(hex) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun hexDecode64OrNull() {
|
||||
r.measureRepeated { Hex.decode64OrNull(hex) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun hexEncode64() {
|
||||
r.measureRepeated { Hex.encode64(bytes) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun hexDecode128() {
|
||||
r.measureRepeated { Hex.decode128(hex128) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun hexEncode128() {
|
||||
r.measureRepeated { Hex.encode128(bytes64) }
|
||||
}
|
||||
|
||||
/** The pre-existing two-pass way to safely decode an id, for comparison with [hexDecode64OrNull]. */
|
||||
@Test
|
||||
fun hexIsHex64ThenDecode() {
|
||||
r.measureRepeated { if (Hex.isHex64(hex)) Hex.decode(hex) else null }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun hexToLong256() {
|
||||
r.measureRepeated { Hex.toLong256(hex) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,9 +185,15 @@ object Hex {
|
||||
require(hex.length and 1 == 0) {
|
||||
"Invalid hex $hex"
|
||||
}
|
||||
return ByteArray(hex.length / 2) {
|
||||
(hexToByte[hex[2 * it].code] shl 4 or hexToByte[hex[2 * it + 1].code]).toByte()
|
||||
// table hoisted into a local: the JVM/ART doesn't reliably prove the
|
||||
// field load loop-invariant, and re-loading it per char costs ~25%
|
||||
val table = hexToByte
|
||||
val out = ByteArray(hex.length shr 1)
|
||||
var c = 0
|
||||
for (i in out.indices) {
|
||||
out[i] = ((table[hex[c++].code] shl 4) or table[hex[c++].code]).toByte()
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -268,10 +274,11 @@ object Hex {
|
||||
|
||||
/** Encodes [input] as a lower-case hex string (two chars per byte). */
|
||||
fun encode(input: ByteArray): String {
|
||||
val table = byteToHex
|
||||
val out = CharArray(input.size * 2)
|
||||
var outIdx = 0
|
||||
for (i in 0 until input.size) {
|
||||
val chars = byteToHex[input[i].toInt() and 0xFF]
|
||||
val chars = table[input[i].toInt() and 0xFF]
|
||||
out[outIdx++] = (chars shr 8).toChar()
|
||||
out[outIdx++] = (chars and 0xFF).toChar()
|
||||
}
|
||||
@@ -290,23 +297,26 @@ object Hex {
|
||||
fun readLong(
|
||||
hex: String,
|
||||
offset: Int,
|
||||
): Long =
|
||||
(hexToByte[hex[offset].code].toLong() shl 60) or
|
||||
(hexToByte[hex[offset + 1].code].toLong() shl 56) or
|
||||
(hexToByte[hex[offset + 2].code].toLong() shl 52) or
|
||||
(hexToByte[hex[offset + 3].code].toLong() shl 48) or
|
||||
(hexToByte[hex[offset + 4].code].toLong() shl 44) or
|
||||
(hexToByte[hex[offset + 5].code].toLong() shl 40) or
|
||||
(hexToByte[hex[offset + 6].code].toLong() shl 36) or
|
||||
(hexToByte[hex[offset + 7].code].toLong() shl 32) or
|
||||
(hexToByte[hex[offset + 8].code].toLong() shl 28) or
|
||||
(hexToByte[hex[offset + 9].code].toLong() shl 24) or
|
||||
(hexToByte[hex[offset + 10].code].toLong() shl 20) or
|
||||
(hexToByte[hex[offset + 11].code].toLong() shl 16) or
|
||||
(hexToByte[hex[offset + 12].code].toLong() shl 12) or
|
||||
(hexToByte[hex[offset + 13].code].toLong() shl 8) or
|
||||
(hexToByte[hex[offset + 14].code].toLong() shl 4) or
|
||||
hexToByte[hex[offset + 15].code].toLong()
|
||||
): Long {
|
||||
// table hoisted into a local — one field load instead of sixteen
|
||||
val t = hexToByte
|
||||
return (t[hex[offset].code].toLong() shl 60) or
|
||||
(t[hex[offset + 1].code].toLong() shl 56) or
|
||||
(t[hex[offset + 2].code].toLong() shl 52) or
|
||||
(t[hex[offset + 3].code].toLong() shl 48) or
|
||||
(t[hex[offset + 4].code].toLong() shl 44) or
|
||||
(t[hex[offset + 5].code].toLong() shl 40) or
|
||||
(t[hex[offset + 6].code].toLong() shl 36) or
|
||||
(t[hex[offset + 7].code].toLong() shl 32) or
|
||||
(t[hex[offset + 8].code].toLong() shl 28) or
|
||||
(t[hex[offset + 9].code].toLong() shl 24) or
|
||||
(t[hex[offset + 10].code].toLong() shl 20) or
|
||||
(t[hex[offset + 11].code].toLong() shl 16) or
|
||||
(t[hex[offset + 12].code].toLong() shl 12) or
|
||||
(t[hex[offset + 13].code].toLong() shl 8) or
|
||||
(t[hex[offset + 14].code].toLong() shl 4) or
|
||||
t[hex[offset + 15].code].toLong()
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the first 64 bits (16 hex chars) of [hex] as a single [Long].
|
||||
@@ -350,9 +360,10 @@ object Hex {
|
||||
id: String,
|
||||
ourId: ByteArray,
|
||||
): Boolean {
|
||||
val table = byteToHex
|
||||
var charIndex = 0
|
||||
for (i in 0 until ourId.size) {
|
||||
val chars = byteToHex[ourId[i].toInt() and 0xFF]
|
||||
val chars = table[ourId[i].toInt() and 0xFF]
|
||||
if (
|
||||
id[charIndex++] != (chars shr 8).toChar() ||
|
||||
id[charIndex++] != (chars and 0xFF).toChar()
|
||||
|
||||
Reference in New Issue
Block a user