30% Faster isHex for strings with 64 bytes.

This commit is contained in:
Vitor Pamplona
2025-10-03 15:58:01 -04:00
parent 21c1d705a1
commit 7994945209
3 changed files with 133 additions and 19 deletions

View File

@@ -100,17 +100,7 @@ class HexBenchmark {
}
@Test
fun newIsHex() {
val isHexChar =
BooleanArray(256).apply {
"0123456789abcdefABCDEF".forEach { this[it.code] = true }
}
r.measureRepeated {
for (c in hex.indices) {
if (!isHexChar[hex[c].code]) return@measureRepeated
}
return@measureRepeated
}
fun isHex64() {
r.measureRepeated { Hex.isHex64(hex) }
}
}

View File

@@ -83,17 +83,50 @@ class HexEncodingTest {
assertFalse("`a", Hex.isHex("`a"))
assertFalse("gg", Hex.isHex("gg"))
assertFalse("fg", Hex.isHex("fg"))
assertFalse("\uD83E\uDD70", Hex.isHex("\uD83E\uDD70"))
}
@OptIn(ExperimentalStdlibApi::class)
@Test
fun testRandomsIsHex() {
val lowerCaseHexNeg = "ghijklmnopqrstuvwxyz"
val upperCaseHexNeg = "GHIJKLMNOPQRSTUVWXYZ"
for (i in 0..10000) {
val bytes = RandomInstance.bytes(32)
val hex = bytes.toHexString(HexFormat.Default)
assertTrue(hex, Hex.isHex(hex))
val hexUpper = bytes.toHexString(HexFormat.UpperCase)
assertTrue(hexUpper, Hex.isHex(hexUpper))
// scramble
val negHex = hex.replaceFirst(hex.random(), lowerCaseHexNeg.random())
val negHexUpper = hexUpper.replaceFirst(hexUpper.random(), upperCaseHexNeg.random())
assertFalse(negHex, Hex.isHex(negHex))
assertFalse(negHexUpper, Hex.isHex(negHexUpper))
}
}
@OptIn(ExperimentalStdlibApi::class)
@Test
fun testRandomsIsHex64() {
val lowerCaseHexNeg = "ghijklmnopqrstuvwxyz"
val upperCaseHexNeg = "GHIJKLMNOPQRSTUVWXYZ"
for (i in 0..10000) {
val bytes = RandomInstance.bytes(32)
val hex = bytes.toHexString(HexFormat.Default)
assertTrue(hex, Hex.isHex64(hex))
val hexUpper = bytes.toHexString(HexFormat.UpperCase)
assertTrue(hexUpper, Hex.isHex64(hexUpper))
// scramble
val negHex = hex.replaceFirst(hex.random(), lowerCaseHexNeg.random())
val negHexUpper = hexUpper.replaceFirst(hexUpper.random(), upperCaseHexNeg.random())
assertFalse(hex + ":" + negHex, Hex.isHex64(negHex))
assertFalse(hexUpper + ":" + negHexUpper, Hex.isHex64(negHexUpper))
}
}

View File

@@ -36,23 +36,114 @@ object Hex {
(LOWER_CASE_HEX[(it shr 4)].code shl 8) or LOWER_CASE_HEX[(it and 0xF)].code
}
// 47ns in debug on the Emulator
fun isHex(hex: String?): Boolean {
if (hex.isNullOrEmpty()) return false
if (hex == null) return false
if (hex.length and 1 != 0) return false
try {
for (c in hex.indices) {
if (c < 0 || c > 255) return false
if (hexToByte[hex[c].code] < 0) return false
}
return try {
internalIsHex(hex, hexToByte)
} catch (_: IllegalArgumentException) {
// there are p tags with emoji's which makes the hex[c].code > 256
return false
false
} catch (_: IndexOutOfBoundsException) {
// there are p tags with emoji's which makes the hex[c].code > 256
false
}
}
// breaking this function away from the main one improves performance for some reason
fun internalIsHex(
hex: String,
hexToByte: IntArray,
): Boolean {
for (c in hex.indices) {
if (hexToByte[hex[c].code] < 0) return false
}
return true
}
// 30% faster than isHex
fun isHex64(hex: String): Boolean =
try {
hexToByte[hex[0].code] >= 0 &&
hexToByte[hex[1].code] >= 0 &&
hexToByte[hex[2].code] >= 0 &&
hexToByte[hex[3].code] >= 0 &&
hexToByte[hex[4].code] >= 0 &&
hexToByte[hex[5].code] >= 0 &&
hexToByte[hex[6].code] >= 0 &&
hexToByte[hex[7].code] >= 0 &&
hexToByte[hex[8].code] >= 0 &&
hexToByte[hex[9].code] >= 0 &&
hexToByte[hex[10].code] >= 0 &&
hexToByte[hex[11].code] >= 0 &&
hexToByte[hex[12].code] >= 0 &&
hexToByte[hex[13].code] >= 0 &&
hexToByte[hex[14].code] >= 0 &&
hexToByte[hex[15].code] >= 0 &&
hexToByte[hex[16].code] >= 0 &&
hexToByte[hex[17].code] >= 0 &&
hexToByte[hex[18].code] >= 0 &&
hexToByte[hex[19].code] >= 0 &&
hexToByte[hex[20].code] >= 0 &&
hexToByte[hex[21].code] >= 0 &&
hexToByte[hex[22].code] >= 0 &&
hexToByte[hex[23].code] >= 0 &&
hexToByte[hex[24].code] >= 0 &&
hexToByte[hex[25].code] >= 0 &&
hexToByte[hex[26].code] >= 0 &&
hexToByte[hex[27].code] >= 0 &&
hexToByte[hex[28].code] >= 0 &&
hexToByte[hex[29].code] >= 0 &&
hexToByte[hex[30].code] >= 0 &&
hexToByte[hex[31].code] >= 0 &&
hexToByte[hex[32].code] >= 0 &&
hexToByte[hex[33].code] >= 0 &&
hexToByte[hex[34].code] >= 0 &&
hexToByte[hex[35].code] >= 0 &&
hexToByte[hex[36].code] >= 0 &&
hexToByte[hex[37].code] >= 0 &&
hexToByte[hex[38].code] >= 0 &&
hexToByte[hex[39].code] >= 0 &&
hexToByte[hex[40].code] >= 0 &&
hexToByte[hex[41].code] >= 0 &&
hexToByte[hex[42].code] >= 0 &&
hexToByte[hex[43].code] >= 0 &&
hexToByte[hex[44].code] >= 0 &&
hexToByte[hex[45].code] >= 0 &&
hexToByte[hex[46].code] >= 0 &&
hexToByte[hex[47].code] >= 0 &&
hexToByte[hex[48].code] >= 0 &&
hexToByte[hex[49].code] >= 0 &&
hexToByte[hex[50].code] >= 0 &&
hexToByte[hex[51].code] >= 0 &&
hexToByte[hex[52].code] >= 0 &&
hexToByte[hex[53].code] >= 0 &&
hexToByte[hex[54].code] >= 0 &&
hexToByte[hex[55].code] >= 0 &&
hexToByte[hex[56].code] >= 0 &&
hexToByte[hex[57].code] >= 0 &&
hexToByte[hex[58].code] >= 0 &&
hexToByte[hex[59].code] >= 0 &&
hexToByte[hex[60].code] >= 0 &&
hexToByte[hex[61].code] >= 0 &&
hexToByte[hex[62].code] >= 0 &&
hexToByte[hex[63].code] >= 0
} catch (_: IllegalArgumentException) {
// there are p tags with emoji's which makes the hex[c].code > 256
false
} catch (_: IndexOutOfBoundsException) {
// there are p tags with emoji's which makes the hex[c].code > 256
false
}
fun decode(hex: String): ByteArray {
// faster version of hex decoder
require(hex.length and 1 == 0)