diff --git a/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/HexBenchmark.kt b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/HexBenchmark.kt index 64aa1cf179..3b11aa84df 100644 --- a/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/HexBenchmark.kt +++ b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/HexBenchmark.kt @@ -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) } + } } diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/Hex.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/Hex.kt index b6434e3a8b..1b7a0e542d 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/Hex.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/Hex.kt @@ -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()