Improving performance of the Hex encoder.

This commit is contained in:
Vitor Pamplona 2024-12-03 14:07:13 -05:00
parent 869debaf9d
commit b9883093ac
4 changed files with 97 additions and 68 deletions

View File

@ -46,7 +46,7 @@ class RichTextParserBenchmark {
fun parseApkUrl() {
benchmarkRule.measureRepeated {
assertNull(
RichTextParser().parseMediaUrl(
RichTextParser().createMediaContent(
"https://github.com/vitorpamplona/amethyst/releases/download/v0.83.10/amethyst-googleplay-universal-v0.83.10.apk",
EmptyTagList,
null,

View File

@ -24,7 +24,6 @@ import androidx.benchmark.junit4.BenchmarkRule
import androidx.benchmark.junit4.measureRepeated
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.vitorpamplona.quartz.encoders.HexValidator
import junit.framework.TestCase.assertEquals
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@ -37,58 +36,59 @@ import org.junit.runner.RunWith
*/
@RunWith(AndroidJUnit4::class)
class HexBenchmark {
@get:Rule val benchmarkRule = BenchmarkRule()
@get:Rule val r = BenchmarkRule()
val testHex = "48a72b485d38338627ec9d427583551f9af4f016c739b8ec0d6313540a8b12cf"
val hex = "48a72b485d38338627ec9d427583551f9af4f016c739b8ec0d6313540a8b12cf"
val bytes =
fr.acinq.secp256k1.Hex
.decode(hex)
@Test
fun hexDecodeOurs() {
benchmarkRule.measureRepeated {
r.measureRepeated {
com.vitorpamplona.quartz.encoders.Hex
.decode(testHex)
.decode(hex)
}
}
@Test
fun hexEncodeOurs() {
val bytes =
r.measureRepeated {
com.vitorpamplona.quartz.encoders.Hex
.decode(testHex)
benchmarkRule.measureRepeated {
assertEquals(
testHex,
com.vitorpamplona.quartz.encoders.Hex
.encode(bytes),
)
.encode(bytes)
}
}
@Test
fun hexDecodeBaseSecp() {
benchmarkRule.measureRepeated {
r.measureRepeated {
fr.acinq.secp256k1.Hex
.decode(testHex)
.decode(hex)
}
}
@Test
fun hexEncodeBaseSecp() {
val bytes =
r.measureRepeated {
fr.acinq.secp256k1.Hex
.decode(testHex)
benchmarkRule.measureRepeated {
assertEquals(
testHex,
fr.acinq.secp256k1.Hex
.encode(bytes),
)
.encode(bytes)
}
}
@OptIn(ExperimentalStdlibApi::class)
@Test
fun hexDecodeKotlin() {
r.measureRepeated { hex.hexToByteArray(HexFormat.Default) }
}
@OptIn(ExperimentalStdlibApi::class)
@Test
fun hexEncodeKotlin() {
r.measureRepeated { bytes.toHexString(HexFormat.Default) }
}
@Test
fun isHex() {
benchmarkRule.measureRepeated { HexValidator.isHex(testHex) }
r.measureRepeated { HexValidator.isHex(hex) }
}
}

View File

@ -23,6 +23,8 @@ package com.vitorpamplona.quartz.encoders
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.vitorpamplona.quartz.crypto.CryptoUtils
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
@ -69,4 +71,43 @@ class HexEncodingTest {
)
}
}
@Test
fun testIsHex() {
assertFalse("/0", HexValidator.isHex("/0"))
assertFalse("/.", HexValidator.isHex("/."))
assertFalse("!!", HexValidator.isHex("!!"))
assertFalse("::", HexValidator.isHex("::"))
assertFalse("@@", HexValidator.isHex("@@"))
assertFalse("GG", HexValidator.isHex("GG"))
assertFalse("FG", HexValidator.isHex("FG"))
assertFalse("`a", HexValidator.isHex("`a"))
assertFalse("gg", HexValidator.isHex("gg"))
assertFalse("fg", HexValidator.isHex("fg"))
}
@OptIn(ExperimentalStdlibApi::class)
@Test
fun testRandomsIsHex() {
for (i in 0..10000) {
val bytes = CryptoUtils.privkeyCreate()
val hex = bytes.toHexString(HexFormat.Default)
assertTrue(hex, HexValidator.isHex(hex))
val hexUpper = bytes.toHexString(HexFormat.UpperCase)
assertTrue(hexUpper, HexValidator.isHex(hexUpper))
}
}
@OptIn(ExperimentalStdlibApi::class)
@Test
fun testRandomsUppercase() {
for (i in 0..1000) {
val bytes = CryptoUtils.privkeyCreate()
val hex = bytes.toHexString(HexFormat.UpperCase)
assertEquals(
bytes.toList(),
Hex.decode(hex).toList(),
)
}
}
}

View File

@ -27,65 +27,53 @@ fun ByteArray.toHexKey(): HexKey = Hex.encode(this)
fun HexKey.hexToByteArray(): ByteArray = Hex.decode(this)
object HexValidator {
private fun isHexChar(c: Char): Boolean =
when (c) {
in '0'..'9' -> true
in 'a'..'f' -> true
in 'A'..'F' -> true
else -> false
}
val lowerCaseHex = arrayOf('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f')
val upperCaseHex = arrayOf('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F')
val hexToByte: IntArray =
IntArray(256) { -1 }.apply {
lowerCaseHex.forEachIndexed { index, char -> this[char.code] = index }
upperCaseHex.forEachIndexed { index, char -> this[char.code] = index }
}
// Encodes both chars in a single Int variable
val byteToHex =
IntArray(256) {
(lowerCaseHex[(it shr 4)].code shl 8) or lowerCaseHex[(it and 0xF)].code
}
object HexValidator {
fun isHex(hex: String?): Boolean {
if (hex == null) return false
if (hex.isEmpty()) return false
if (hex.length % 2 != 0) return false // must be even
var isHex = true
if (hex.length and 1 != 0) return false // must be even
for (c in hex) {
if (!isHexChar(c)) {
isHex = false
break
}
for (c in hex.indices) {
if (hexToByte[hex[c].code] < 0) return false
}
return isHex
return true
}
}
object Hex {
val hexCode =
arrayOf('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f')
// Faster if no calculations are needed.
private fun hexToBin(ch: Char): Int =
when (ch) {
in '0'..'9' -> ch - '0'
in 'a'..'f' -> ch - 'a' + 10
in 'A'..'F' -> ch - 'A' + 10
else -> throw IllegalArgumentException("illegal hex character: $ch")
}
@JvmStatic
fun decode(hex: String): ByteArray {
// faster version of hex decoder
require(hex.length % 2 == 0)
val outSize = hex.length / 2
val out = ByteArray(outSize)
for (i in 0 until outSize) {
out[i] = (hexToBin(hex[2 * i]) * 16 + hexToBin(hex[2 * i + 1])).toByte()
require(hex.length and 1 == 0)
return ByteArray(hex.length / 2) {
(hexToByte[hex[2 * it].code] shl 4 or hexToByte[hex[2 * it + 1].code]).toByte()
}
return out
}
@JvmStatic
fun encode(input: ByteArray): String {
val len = input.size
val out = CharArray(len * 2)
for (i in 0 until len) {
out[i * 2] = hexCode[(input[i].toInt() shr 4) and 0xF]
out[i * 2 + 1] = hexCode[input[i].toInt() and 0xF]
val out = CharArray(input.size * 2)
var outIdx = 0
for (i in 0 until input.size) {
val chars = byteToHex[input[i].toInt() and 0xFF]
out[outIdx++] = (chars shr 8).toChar()
out[outIdx++] = (chars and 0xFF).toChar()
}
return String(out)
}