Skip to content

Commit 89d857c

Browse files
committed
faster hex encoding
1 parent 9c4b0d8 commit 89d857c

File tree

3 files changed

+124
-22
lines changed

3 files changed

+124
-22
lines changed

Cargo.toml

+13-1
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

36-
[dev_dependencies]
37+
[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

+46
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

+65-21
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,36 @@ 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+
let alphabet = match UPPER {
24+
true => b"0123456789ABCDEF",
25+
false => b"0123456789abcdef",
26+
};
27+
28+
dst.chunks_exact_mut(2).zip(src).for_each(|(s, c)| {
29+
s[0] = alphabet[(c >> 4) as usize];
30+
s[1] = alphabet[(c & 0xF) as usize];
31+
});
32+
}
33+
34+
#[cfg(feature = "faster-hex")]
35+
fn faster_hex_encode<const UPPER: bool>(src: &[u8], dst: &mut [u8]) {
36+
debug_assert!(dst.len() >= (src.len() * 2));
37+
38+
#[cfg(miri)]
39+
hex_encode_fallback::<UPPER>(src, dst);
40+
41+
// the `unwrap_unchecked` is to avoid the length checks
42+
#[cfg(not(miri))]
43+
match UPPER {
44+
true => unsafe { faster_hex::hex_encode_upper(src, dst).unwrap_unchecked() },
45+
false => unsafe { faster_hex::hex_encode(src, dst).unwrap_unchecked() },
46+
};
47+
}
2348

24-
fn generic_hex<N: ArrayLength>(
49+
fn generic_hex<N: ArrayLength, const UPPER: bool>(
2550
arr: &GenericArray<u8, N>,
26-
alphabet: &[u8; 16], // use fixed-length array to avoid slice index checks
2751
f: &mut fmt::Formatter<'_>,
2852
) -> fmt::Result
2953
where
@@ -36,32 +60,50 @@ where
3660
_ => max_digits,
3761
};
3862

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

4175
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();
76+
// For small arrays use a stack allocated buffer of 2x number of bytes
77+
let mut buf = GenericArray::<u8, Sum<N, N>>::default();
4578

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-
});
79+
if N::USIZE < 16 {
80+
// for the smallest inputs, don't bother limiting to max_bytes,
81+
// just process the entire array. When "faster-hex" is enabled,
82+
// this avoids its logic that winds up going to the fallback anyway
83+
hex_encode_fallback::<UPPER>(arr, &mut buf);
84+
} else if cfg!(not(feature = "faster-hex")) {
85+
hex_encode_fallback::<UPPER>(input, &mut buf);
86+
} else {
87+
#[cfg(feature = "faster-hex")]
88+
faster_hex_encode::<UPPER>(input, &mut buf);
89+
}
5090

51-
f.write_str(unsafe { str::from_utf8_unchecked(&res[..max_digits]) })?;
91+
f.write_str(unsafe { str::from_utf8_unchecked(buf.get_unchecked(..max_digits)) })?;
5292
} else {
5393
// For large array use chunks of up to 1024 bytes (2048 hex chars)
5494
let mut buf = [0u8; 2048];
5595
let mut digits_left = max_digits;
5696

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-
});
97+
for chunk in input.chunks(1024) {
98+
#[cfg(feature = "faster-hex")]
99+
faster_hex_encode::<UPPER>(chunk, &mut buf);
100+
101+
#[cfg(not(feature = "faster-hex"))]
102+
hex_encode_fallback::<UPPER>(chunk, buf);
62103

63104
let n = min(chunk.len() * 2, digits_left);
64-
f.write_str(unsafe { str::from_utf8_unchecked(&buf[..n]) })?;
105+
// SAFETY: n will always be within bounds due to the above min
106+
f.write_str(unsafe { str::from_utf8_unchecked(buf.get_unchecked(..n)) })?;
65107
digits_left -= n;
66108
}
67109
}
@@ -73,8 +115,9 @@ where
73115
N: Add<N>,
74116
Sum<N, N>: ArrayLength,
75117
{
118+
#[inline]
76119
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
77-
generic_hex(self, &LOWER_CHARS, f)
120+
generic_hex::<_, false>(self, f)
78121
}
79122
}
80123

@@ -83,7 +126,8 @@ where
83126
N: Add<N>,
84127
Sum<N, N>: ArrayLength,
85128
{
129+
#[inline]
86130
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
87-
generic_hex(self, &UPPER_CHARS, f)
131+
generic_hex::<_, true>(self, f)
88132
}
89133
}

0 commit comments

Comments
 (0)