Skip to content

Commit 9b12a1a

Browse files
authored
Faster hex encoding (#144)
* faster hex encoding
1 parent 5aa4e27 commit 9b12a1a

File tree

3 files changed

+118
-21
lines changed

3 files changed

+118
-21
lines changed

Cargo.toml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,23 @@ typenum = { version = "1.16", features = ["const-generics"] }
3232
const-default = { version = "1", optional = true, default-features = false }
3333
serde = { version = "1.0", optional = true, default-features = false }
3434
zeroize = { version = "1", optional = true, default-features = false }
35+
faster-hex = { version = "0.8", optional = true, default-features = false }
3536

3637
[dev-dependencies]
3738
# this can't yet be made optional, see https://github.com/rust-lang/cargo/issues/1596
3839
serde_json = "1.0"
3940
bincode = "1.0"
41+
criterion = { version = "0.5", features = ["html_reports"] }
42+
rand = "0.8"
43+
44+
[[bench]]
45+
name = "hex"
46+
harness = false
47+
48+
[profile.bench]
49+
opt-level = 3
50+
lto = 'fat'
51+
codegen-units = 1
4052

4153
[package.metadata.docs.rs]
4254
# all but "internals", don't show those on docs.rs

benches/hex.rs

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
use criterion::{
2+
criterion_group, criterion_main, measurement::WallTime, BenchmarkGroup, Criterion,
3+
};
4+
use generic_array::{typenum::*, ArrayLength, GenericArray};
5+
use rand::RngCore;
6+
7+
use std::{fmt::UpperHex, io::Write};
8+
9+
fn criterion_benchmark(c: &mut Criterion) {
10+
let mut hex = c.benchmark_group("hex");
11+
12+
let mut rng = rand::thread_rng();
13+
14+
macro_rules! all_hex_benches {
15+
($($len:ty,)*) => {
16+
$(bench_hex::<$len>(&mut rng, &mut hex);)*
17+
}
18+
}
19+
20+
all_hex_benches!(
21+
U1, U2, U4, U8, U12, U15, U16, U32, U64, U100, U128, U160, U255, U256, U500, U512, U900,
22+
U1023, U1024, Sum<U1024, U1>, U2048, U4096, Prod<U1000, U5>, U10000,
23+
);
24+
25+
hex.finish();
26+
}
27+
28+
criterion_group!(benches, criterion_benchmark);
29+
criterion_main!(benches);
30+
31+
fn bench_hex<N: ArrayLength>(mut rng: impl RngCore, g: &mut BenchmarkGroup<'_, WallTime>)
32+
where
33+
GenericArray<u8, N>: UpperHex,
34+
{
35+
let mut fixture = Box::<GenericArray<u8, N>>::default();
36+
rng.fill_bytes(fixture.as_mut_slice());
37+
38+
g.bench_function(format!("N{:08}", N::USIZE), |b| {
39+
let mut out = Vec::with_capacity(N::USIZE * 2);
40+
41+
b.iter(|| {
42+
_ = write!(out, "{:X}", &*fixture);
43+
out.clear();
44+
});
45+
});
46+
}

src/hex.rs

Lines changed: 60 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,40 @@ use typenum::*;
1818

1919
use crate::{ArrayLength, GenericArray};
2020

21-
static LOWER_CHARS: [u8; 16] = *b"0123456789abcdef";
22-
static UPPER_CHARS: [u8; 16] = *b"0123456789ABCDEF";
21+
#[inline(always)]
22+
fn hex_encode_fallback<const UPPER: bool>(src: &[u8], dst: &mut [u8]) {
23+
if dst.len() < src.len() * 2 {
24+
unsafe { core::hint::unreachable_unchecked() };
25+
}
26+
27+
let alphabet = match UPPER {
28+
true => b"0123456789ABCDEF",
29+
false => b"0123456789abcdef",
30+
};
31+
32+
dst.chunks_exact_mut(2).zip(src).for_each(|(s, c)| {
33+
s[0] = alphabet[(c >> 4) as usize];
34+
s[1] = alphabet[(c & 0xF) as usize];
35+
});
36+
}
37+
38+
#[inline]
39+
fn hex_encode<const UPPER: bool>(src: &[u8], dst: &mut [u8]) {
40+
debug_assert!(dst.len() >= (src.len() * 2));
2341

24-
fn generic_hex<N: ArrayLength>(
42+
#[cfg(any(miri, not(feature = "faster-hex")))]
43+
hex_encode_fallback::<UPPER>(src, dst);
44+
45+
// the `unwrap_unchecked` is to avoid the length checks
46+
#[cfg(all(feature = "faster-hex", not(miri)))]
47+
match UPPER {
48+
true => unsafe { faster_hex::hex_encode_upper(src, dst).unwrap_unchecked() },
49+
false => unsafe { faster_hex::hex_encode(src, dst).unwrap_unchecked() },
50+
};
51+
}
52+
53+
fn generic_hex<N: ArrayLength, const UPPER: bool>(
2554
arr: &GenericArray<u8, N>,
26-
alphabet: &[u8; 16], // use fixed-length array to avoid slice index checks
2755
f: &mut fmt::Formatter<'_>,
2856
) -> fmt::Result
2957
where
@@ -36,32 +64,43 @@ where
3664
_ => max_digits,
3765
};
3866

39-
let max_hex = (max_digits >> 1) + (max_digits & 1);
67+
// ceil(max_digits / 2)
68+
let max_bytes = (max_digits >> 1) + (max_digits & 1);
69+
70+
let input = {
71+
// LLVM can't seem to automatically prove this
72+
if max_bytes > N::USIZE {
73+
unsafe { core::hint::unreachable_unchecked() };
74+
}
75+
76+
&arr[..max_bytes]
77+
};
4078

4179
if N::USIZE <= 1024 {
42-
// For small arrays use a stack allocated
43-
// buffer of 2x number of bytes
44-
let mut res = GenericArray::<u8, Sum<N, N>>::default();
80+
// For small arrays use a stack allocated buffer of 2x number of bytes
81+
let mut buf = GenericArray::<u8, Sum<N, N>>::default();
4582

46-
arr.iter().take(max_hex).enumerate().for_each(|(i, c)| {
47-
res[i * 2] = alphabet[(c >> 4) as usize];
48-
res[i * 2 + 1] = alphabet[(c & 0xF) as usize];
49-
});
83+
if N::USIZE < 16 {
84+
// for the smallest inputs, don't bother limiting to max_bytes,
85+
// just process the entire array. When "faster-hex" is enabled,
86+
// this avoids its logic that winds up going to the fallback anyway
87+
hex_encode_fallback::<UPPER>(arr, &mut buf);
88+
} else {
89+
hex_encode::<UPPER>(input, &mut buf);
90+
}
5091

51-
f.write_str(unsafe { str::from_utf8_unchecked(&res[..max_digits]) })?;
92+
f.write_str(unsafe { str::from_utf8_unchecked(buf.get_unchecked(..max_digits)) })?;
5293
} else {
5394
// For large array use chunks of up to 1024 bytes (2048 hex chars)
5495
let mut buf = [0u8; 2048];
5596
let mut digits_left = max_digits;
5697

57-
for chunk in arr[..max_hex].chunks(1024) {
58-
chunk.iter().enumerate().for_each(|(i, c)| {
59-
buf[i * 2] = alphabet[(c >> 4) as usize];
60-
buf[i * 2 + 1] = alphabet[(c & 0xF) as usize];
61-
});
98+
for chunk in input.chunks(1024) {
99+
hex_encode::<UPPER>(chunk, &mut buf);
62100

63101
let n = min(chunk.len() * 2, digits_left);
64-
f.write_str(unsafe { str::from_utf8_unchecked(&buf[..n]) })?;
102+
// SAFETY: n will always be within bounds due to the above min
103+
f.write_str(unsafe { str::from_utf8_unchecked(buf.get_unchecked(..n)) })?;
65104
digits_left -= n;
66105
}
67106
}
@@ -74,7 +113,7 @@ where
74113
Sum<N, N>: ArrayLength,
75114
{
76115
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
77-
generic_hex(self, &LOWER_CHARS, f)
116+
generic_hex::<_, false>(self, f)
78117
}
79118
}
80119

@@ -84,6 +123,6 @@ where
84123
Sum<N, N>: ArrayLength,
85124
{
86125
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
87-
generic_hex(self, &UPPER_CHARS, f)
126+
generic_hex::<_, true>(self, f)
88127
}
89128
}

0 commit comments

Comments
 (0)