commit d66028551bc19183089ab4df0b7ecfaa68f9bf81
parent 95c2f76677ca08b5ce1ce603cd2229f00ff885d1
Author: Antoine A <>
Date: Thu, 2 Jan 2025 18:36:28 +0100
taler-common: optimize base32 encoding
Diffstat:
4 files changed, 74 insertions(+), 34 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
@@ -1236,10 +1236,13 @@ checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674"
[[package]]
name = "jiff"
-version = "0.1.16"
+version = "0.1.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "24a46169c7a10358cdccfb179910e8a5a392fc291bdb409da9aeece5b19786d8"
+checksum = "943611a469f78ab9afdac9022e473a80fca16a9deca6c5be3eb566d872231e76"
dependencies = [
+ "log",
+ "portable-atomic",
+ "portable-atomic-util",
"serde",
]
@@ -1633,6 +1636,21 @@ dependencies = [
]
[[package]]
+name = "portable-atomic"
+version = "1.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "280dc24453071f1b63954171985a0b0d30058d287960968b9b2aca264c8d4ee6"
+
+[[package]]
+name = "portable-atomic-util"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507"
+dependencies = [
+ "portable-atomic",
+]
+
+[[package]]
name = "powerfmt"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2361,9 +2379,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]]
name = "syn"
-version = "2.0.93"
+version = "2.0.94"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9c786062daee0d6db1132800e623df74274a0a87322d8e183338e01b3d98d058"
+checksum = "987bc0be1cdea8b10216bd06e2ca407d40b9543468fafd3ddfb02f36e77f71f3"
dependencies = [
"proc-macro2",
"quote",
diff --git a/taler-api/tests/api.rs b/taler-api/tests/api.rs
@@ -251,9 +251,7 @@ async fn incoming_history(pool: PgPool) {
.map(|it| match it {
IncomingBankTransaction::Reserve { row_id, .. }
| IncomingBankTransaction::Wad { row_id, .. }
- | IncomingBankTransaction::Kyc { row_id, .. } => {
- *row_id as i64
- }
+ | IncomingBankTransaction::Kyc { row_id, .. } => *row_id as i64,
})
.collect()
},
diff --git a/taler-common/benches/base32.rs b/taler-common/benches/base32.rs
@@ -15,10 +15,10 @@
*/
use criterion::{black_box, criterion_group, criterion_main, Criterion};
-use taler_common::types::base32::{decode, encode, Base32};
+use taler_common::types::base32::{decode, encode_static, encoded_batch_len, Base32};
fn parser(c: &mut Criterion) {
- let mut rng = fastrand::Rng::with_seed(42);
+ let mut rng = fastrand::Rng::new();
let encode_random: Vec<_> = (0..30)
.map(|_| {
let mut bytes = [0; 64];
@@ -31,21 +31,22 @@ fn parser(c: &mut Criterion) {
.map(|_| (0..56).map(|_| rng.char(..)).collect())
.collect();
- c.bench_function("encode_random", |b| {
+ let mut buf = [0u8; encoded_batch_len(64)];
+ c.bench_function("base32_encode_random", |b| {
b.iter(|| {
for case in &encode_random {
- encode(black_box(case.as_ref()));
+ encode_static(black_box(case), &mut buf);
}
})
});
- c.bench_function("decode_valid", |b| {
+ c.bench_function("base32_decode_valid", |b| {
b.iter(|| {
for case in &decode_valid {
decode::<32>(black_box(case.as_str())).ok();
}
})
});
- c.bench_function("decode_randoms", |b| {
+ c.bench_function("base32_decode_random", |b| {
b.iter(|| {
for case in &decode_randoms {
decode::<32>(black_box(case.as_str())).ok();
diff --git a/taler-common/src/types/base32.rs b/taler-common/src/types/base32.rs
@@ -20,32 +20,54 @@ use serde::{de::Error, Deserialize, Deserializer, Serialize, Serializer};
pub const CROCKFORD_ALPHABET: &[u8] = b"0123456789ABCDEFGHJKMNPQRSTVWXYZ";
-/** Encode bytes into a Crockford's base32 string */
-pub fn encode(bytes: &[u8]) -> String {
- // Pre allocate for all chunks
- let mut encoded = Vec::with_capacity((bytes.len() / 5 + 1) * 8);
+/** Encoded bytes len of Crockford's base32 */
+#[inline]
+pub const fn encoded_len(len: usize) -> usize {
+ return (len * 8 + 4) / 5;
+}
+
+/** Buffer bytes len of Crockford's base32 using a batch of 8 chars */
+#[inline]
+pub const fn encoded_batch_len(len: usize) -> usize {
+ return (len / 5 + 1) * 8;
+}
+
+/** Encode bytes using Crockford's base32 */
+#[inline]
+pub fn encode_static<'a, const N: usize>(bytes: &[u8; N], out: &'a mut [u8]) -> &'a str {
+ // Batch encoded
+ encode_batch(bytes, out);
+
+ // Truncate incomplete ending chunk
+ let truncated = &out[..encoded_len(bytes.len())];
+
+ // SAFETY: only contains valid ASCII characters from CROCKFORD_ALPHABET
+ unsafe { std::str::from_utf8_unchecked(&truncated) }
+}
+
+/** Batch encode bytes using Crockford's base32 */
+#[inline]
+fn encode_batch(bytes: &[u8], out: &mut [u8]) {
+ // Check buffer len
+ assert!(out.len() >= encoded_batch_len(bytes.len()));
+
// Encode chunks of 5B for 8 chars
- for chunk in bytes.chunks(5) {
+ for (i, chunk) in bytes.chunks(5).enumerate() {
let mut buf = [0u8; 5];
for (i, &b) in chunk.iter().enumerate() {
buf[i] = b;
}
- encoded.extend_from_slice(&[
- CROCKFORD_ALPHABET[((buf[0] & 0xF8) >> 3) as usize],
- CROCKFORD_ALPHABET[(((buf[0] & 0x07) << 2) | ((buf[1] & 0xC0) >> 6)) as usize],
- CROCKFORD_ALPHABET[((buf[1] & 0x3E) >> 1) as usize],
- CROCKFORD_ALPHABET[(((buf[1] & 0x01) << 4) | ((buf[2] & 0xF0) >> 4)) as usize],
- CROCKFORD_ALPHABET[(((buf[2] & 0x0F) << 1) | (buf[3] >> 7)) as usize],
- CROCKFORD_ALPHABET[((buf[3] & 0x7C) >> 2) as usize],
- CROCKFORD_ALPHABET[(((buf[3] & 0x03) << 3) | ((buf[4] & 0xE0) >> 5)) as usize],
- CROCKFORD_ALPHABET[(buf[4] & 0x1F) as usize],
- ]);
+ // SAFETY: we only write at most encoded_batch_len bytes
+ let out = unsafe { out.get_unchecked_mut(i * 8..(i + 1) * 8) };
+ out[0] = CROCKFORD_ALPHABET[((buf[0] & 0xF8) >> 3) as usize];
+ out[1] = CROCKFORD_ALPHABET[(((buf[0] & 0x07) << 2) | ((buf[1] & 0xC0) >> 6)) as usize];
+ out[2] = CROCKFORD_ALPHABET[((buf[1] & 0x3E) >> 1) as usize];
+ out[3] = CROCKFORD_ALPHABET[(((buf[1] & 0x01) << 4) | ((buf[2] & 0xF0) >> 4)) as usize];
+ out[4] = CROCKFORD_ALPHABET[(((buf[2] & 0x0F) << 1) | (buf[3] >> 7)) as usize];
+ out[5] = CROCKFORD_ALPHABET[((buf[3] & 0x7C) >> 2) as usize];
+ out[6] = CROCKFORD_ALPHABET[(((buf[3] & 0x03) << 3) | ((buf[4] & 0xE0) >> 5)) as usize];
+ out[7] = CROCKFORD_ALPHABET[(buf[4] & 0x1F) as usize];
}
- // Truncate incomplete ending chunk
- encoded.truncate((bytes.len() * 8 + 4) / 5);
-
- // SAFETY: only contains valid ASCII characters from CROCKFORD_ALPHABET
- unsafe { String::from_utf8_unchecked(encoded) }
}
#[derive(Debug, thiserror::Error)]
@@ -144,7 +166,8 @@ impl<const L: usize> FromStr for Base32<L> {
impl<const L: usize> Display for Base32<L> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- f.write_str(&encode(&self.0))
+ let mut buff = vec![0u8; encoded_batch_len(L)]; // TODO use a stack allocated buffer when supported
+ f.write_str(encode_static(&self.0, &mut buff))
}
}