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() { fun parseApkUrl() {
benchmarkRule.measureRepeated { benchmarkRule.measureRepeated {
assertNull( assertNull(
RichTextParser().parseMediaUrl( RichTextParser().createMediaContent(
"https://github.com/vitorpamplona/amethyst/releases/download/v0.83.10/amethyst-googleplay-universal-v0.83.10.apk", "https://github.com/vitorpamplona/amethyst/releases/download/v0.83.10/amethyst-googleplay-universal-v0.83.10.apk",
EmptyTagList, EmptyTagList,
null, null,

View File

@ -24,7 +24,6 @@ import androidx.benchmark.junit4.BenchmarkRule
import androidx.benchmark.junit4.measureRepeated import androidx.benchmark.junit4.measureRepeated
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import com.vitorpamplona.quartz.encoders.HexValidator import com.vitorpamplona.quartz.encoders.HexValidator
import junit.framework.TestCase.assertEquals
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
@ -37,58 +36,59 @@ import org.junit.runner.RunWith
*/ */
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class HexBenchmark { 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 @Test
fun hexDecodeOurs() { fun hexDecodeOurs() {
benchmarkRule.measureRepeated { r.measureRepeated {
com.vitorpamplona.quartz.encoders.Hex com.vitorpamplona.quartz.encoders.Hex
.decode(testHex) .decode(hex)
} }
} }
@Test @Test
fun hexEncodeOurs() { fun hexEncodeOurs() {
val bytes = r.measureRepeated {
com.vitorpamplona.quartz.encoders.Hex com.vitorpamplona.quartz.encoders.Hex
.decode(testHex) .encode(bytes)
benchmarkRule.measureRepeated {
assertEquals(
testHex,
com.vitorpamplona.quartz.encoders.Hex
.encode(bytes),
)
} }
} }
@Test @Test
fun hexDecodeBaseSecp() { fun hexDecodeBaseSecp() {
benchmarkRule.measureRepeated { r.measureRepeated {
fr.acinq.secp256k1.Hex fr.acinq.secp256k1.Hex
.decode(testHex) .decode(hex)
} }
} }
@Test @Test
fun hexEncodeBaseSecp() { fun hexEncodeBaseSecp() {
val bytes = r.measureRepeated {
fr.acinq.secp256k1.Hex fr.acinq.secp256k1.Hex
.decode(testHex) .encode(bytes)
benchmarkRule.measureRepeated {
assertEquals(
testHex,
fr.acinq.secp256k1.Hex
.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 @Test
fun isHex() { 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 androidx.test.ext.junit.runners.AndroidJUnit4
import com.vitorpamplona.quartz.crypto.CryptoUtils import com.vitorpamplona.quartz.crypto.CryptoUtils
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith 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) fun HexKey.hexToByteArray(): ByteArray = Hex.decode(this)
object HexValidator { val lowerCaseHex = arrayOf('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f')
private fun isHexChar(c: Char): Boolean = val upperCaseHex = arrayOf('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F')
when (c) {
in '0'..'9' -> true
in 'a'..'f' -> true
in 'A'..'F' -> true
else -> false
}
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 { fun isHex(hex: String?): Boolean {
if (hex == null) return false if (hex == null) return false
if (hex.isEmpty()) return false if (hex.isEmpty()) return false
if (hex.length % 2 != 0) return false // must be even if (hex.length and 1 != 0) return false // must be even
var isHex = true
for (c in hex) { for (c in hex.indices) {
if (!isHexChar(c)) { if (hexToByte[hex[c].code] < 0) return false
isHex = false
break
}
} }
return isHex
return true
} }
} }
object Hex { 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 @JvmStatic
fun decode(hex: String): ByteArray { fun decode(hex: String): ByteArray {
// faster version of hex decoder // faster version of hex decoder
require(hex.length % 2 == 0) require(hex.length and 1 == 0)
val outSize = hex.length / 2 return ByteArray(hex.length / 2) {
val out = ByteArray(outSize) (hexToByte[hex[2 * it].code] shl 4 or hexToByte[hex[2 * it + 1].code]).toByte()
for (i in 0 until outSize) {
out[i] = (hexToBin(hex[2 * i]) * 16 + hexToBin(hex[2 * i + 1])).toByte()
} }
return out
} }
@JvmStatic @JvmStatic
fun encode(input: ByteArray): String { fun encode(input: ByteArray): String {
val len = input.size val out = CharArray(input.size * 2)
val out = CharArray(len * 2) var outIdx = 0
for (i in 0 until len) { for (i in 0 until input.size) {
out[i * 2] = hexCode[(input[i].toInt() shr 4) and 0xF] val chars = byteToHex[input[i].toInt() and 0xFF]
out[i * 2 + 1] = hexCode[input[i].toInt() and 0xF] out[outIdx++] = (chars shr 8).toChar()
out[outIdx++] = (chars and 0xFF).toChar()
} }
return String(out) return String(out)
} }