This commit is contained in:
Michael Pfaff 2022-10-23 11:28:22 -04:00
parent df17979b94
commit c6b4790bdf
Signed by: michael
GPG Key ID: CF402C4A012AA9D4
3 changed files with 410 additions and 56 deletions

View File

@ -7,6 +7,7 @@ edition = "2021"
[dev-dependencies]
criterion = "0.3"
rand = "0.8.5"
[[bench]]
name = "bench"

View File

@ -1,4 +1,7 @@
#![feature(generic_const_exprs)]
#![feature(new_uninit)]
use std::mem::MaybeUninit;
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use fast_hex::*;
@ -9,27 +12,236 @@ const HEX_BYTES: &[u8; ASCII_BYTES.len() * 2] = b"446F6E616C64204A2E205472756D70
const ASCII_BYTES_LONG: &[u8; 256] = b"Donald J. Trump!Donald J. Trump!Donald J. Trump!Donald J. Trump!Donald J. Trump!Donald J. Trump!Donald J. Trump!Donald J. Trump!Donald J. Trump!Donald J. Trump!Donald J. Trump!Donald J. Trump!Donald J. Trump!Donald J. Trump!Donald J. Trump!Donald J. Trump!";
const HEX_BYTES_LONG: &[u8; ASCII_BYTES_LONG.len() * 2] = b"446F6E616C64204A2E205472756D7021446F6E616C64204A2E205472756D7021446F6E616C64204A2E205472756D7021446F6E616C64204A2E205472756D7021446F6E616C64204A2E205472756D7021446F6E616C64204A2E205472756D7021446F6E616C64204A2E205472756D7021446F6E616C64204A2E205472756D7021446F6E616C64204A2E205472756D7021446F6E616C64204A2E205472756D7021446F6E616C64204A2E205472756D7021446F6E616C64204A2E205472756D7021446F6E616C64204A2E205472756D7021446F6E616C64204A2E205472756D7021446F6E616C64204A2E205472756D7021446F6E616C64204A2E205472756D7021";
fn benchmark<const N: usize>(bytes: &[u8; N * 2]) -> impl FnOnce(&mut Criterion) -> () + '_
macro_rules! name {
($group:ident, $f:literal) => {
std::boxed::Box::leak(format!(concat!("{} - ", $f), $group).into_boxed_str())
};
}
//#[track_caller]
fn test_sized<const N: usize, const HEAP_ONLY: bool>(hex_bytes: &[u8; N * 2], bytes: &[u8; N])
where
[(); N * 2]:
[(); N * 2]:,
{
|c| {
c.bench_function("sized", |b| b.iter(|| hex_bytes::<N>(black_box(bytes))));
c.bench_function("dyn unsafe", |b| b.iter(|| hex_bytes_dyn_unsafe(black_box(bytes))));
c.bench_function("dyn unsafe for", |b| b.iter(|| hex_bytes_dyn_unsafe_for(black_box(bytes))));
c.bench_function("dyn unsafe iter", |b| b.iter(|| hex_bytes_dyn_unsafe_iter(black_box(bytes))));
c.bench_function("dyn non-niched", |b| b.iter(|| hex_bytes_dyn(black_box(bytes))));
c.bench_function("dyn niched", |b| b.iter(|| hex_bytes_dyn_niched(black_box(bytes))));
test(hex_bytes, bytes);
if !HEAP_ONLY {
assert_eq!(
Some(bytes),
hex_bytes_sized_const::<N>(hex_bytes).as_ref(),
stringify!(hex_bytes_sized_const)
);
assert_eq!(
Some(bytes),
hex_bytes_sized::<N>(hex_bytes).as_ref(),
stringify!(hex_bytes_sized)
);
}
assert_eq!(
Some(bytes),
hex_bytes_sized_heap::<N>(hex_bytes)
.as_ref()
.map(Box::as_ref),
stringify!(hex_bytes_sized_heap)
);
}
//#[track_caller]
#[inline]
fn test(hex_bytes: &[u8], bytes: &[u8]) {
assert_eq!(hex_bytes.len(), bytes.len() * 2);
assert_eq!(
Some(bytes),
hex_bytes_dyn(hex_bytes).as_ref().map(Box::as_ref),
stringify!(hex_bytes_dyn)
);
assert_eq!(
Some(bytes),
hex_bytes_dyn_niched(hex_bytes).as_ref().map(Box::as_ref),
stringify!(hex_bytes_dyn_niched)
);
assert_eq!(
Some(bytes),
hex_bytes_dyn_unsafe_iter(hex_bytes)
.as_ref()
.map(Box::as_ref),
stringify!(hex_bytes_dyn_unsafe_iter)
);
assert_eq!(
Some(bytes),
hex_bytes_dyn_unsafe(hex_bytes).as_ref().map(Box::as_ref),
stringify!(hex_bytes_dyn_unsafe)
);
}
fn benchmark_sized<const N: usize, const HEAP_ONLY: bool>(
name: &str,
bytes: &[u8; N * 2],
c: &mut Criterion,
) where
[(); N * 2]:,
{
if !HEAP_ONLY {
c.bench_function(name!(name, "sized"), |b| {
b.iter(|| hex_bytes_sized::<N>(black_box(bytes)))
});
c.bench_function(name!(name, "sized const"), |b| {
b.iter(|| hex_bytes_sized_const::<N>(black_box(bytes)))
});
}
c.bench_function(name!(name, "sized heap"), |b| {
b.iter(|| hex_bytes_sized_heap::<N>(black_box(bytes)))
});
benchmark(name, bytes, c);
}
const BENCH_UNSAFE: bool = true;
const BENCH_UNSAFE_ITER: bool = true;
const BENCH_NON_NICHED: bool = false;
const BENCH_NICHED: bool = false;
fn benchmark(name: &str, bytes: &[u8], c: &mut Criterion) {
if BENCH_UNSAFE {
c.bench_function(name!(name, "dyn unsafe"), |b| {
b.iter(|| hex_bytes_dyn_unsafe(black_box(bytes)))
});
}
//c.bench_function(format!("{name} - dyn unsafe for"), |b| b.iter(|| hex_bytes_dyn_unsafe_for(black_box(bytes))));
if BENCH_UNSAFE_ITER {
c.bench_function(name!(name, "dyn unsafe iter"), |b| {
b.iter(|| hex_bytes_dyn_unsafe_iter(black_box(bytes)))
});
}
if BENCH_NON_NICHED {
c.bench_function(name!(name, "dyn non-niched"), |b| {
b.iter(|| hex_bytes_dyn(black_box(bytes)))
});
}
if BENCH_NICHED {
c.bench_function(name!(name, "dyn niched"), |b| {
b.iter(|| hex_bytes_dyn_niched(black_box(bytes)))
});
}
}
pub fn bench_16(c: &mut Criterion) {
benchmark::<{ ASCII_BYTES.len() }>(HEX_BYTES)(c)
test_sized::<{ ASCII_BYTES.len() }, false>(HEX_BYTES, ASCII_BYTES);
benchmark_sized::<{ ASCII_BYTES.len() }, false>("[16]", HEX_BYTES, c);
}
pub fn bench_256(c: &mut Criterion) {
benchmark::<{ ASCII_BYTES_LONG.len() }>(HEX_BYTES_LONG)(c)
test_sized::<{ ASCII_BYTES_LONG.len() }, false>(HEX_BYTES_LONG, ASCII_BYTES_LONG);
benchmark_sized::<{ ASCII_BYTES_LONG.len() }, false>("[256]", HEX_BYTES_LONG, c);
}
criterion_group!(benches, bench_16, bench_256);
const fn __make_hex_chars() -> [u8; 16] {
let mut chars = [0u8; 16];
let mut i = 0u8;
while (i as usize) < chars.len() {
chars[i as usize] = if i < 10 {
'0' as u8 + i
} else {
'a' as u8 + i - 10
};
i += 1;
}
chars
}
const HEX_CHARS: [u8; 16] = __make_hex_chars();
trait SliceRandom {
type Item;
fn choose<R>(&self, rng: &mut R) -> Option<&Self::Item>
where
R: rand::Rng + ?Sized;
}
#[inline]
fn gen_index<R: rand::Rng + ?Sized, const UBOUND: usize>(rng: &mut R, ubound: usize) -> usize {
if UBOUND <= (core::u32::MAX as usize) {
rng.gen_range(0..ubound as u32) as usize
} else {
rng.gen_range(0..ubound)
}
}
impl<T> SliceRandom for [T] {
type Item = T;
#[inline]
fn choose<R>(&self, rng: &mut R) -> Option<&Self::Item>
where
R: rand::Rng + ?Sized,
{
if self.is_empty() {
None
} else {
Some(&self[gen_index::<_, { usize::MAX }>(rng, self.len())])
}
}
}
impl<const N: usize, T> SliceRandom for [T; N] {
type Item = T;
#[inline]
fn choose<R>(&self, rng: &mut R) -> Option<&Self::Item>
where
R: rand::Rng + ?Sized,
{
if self.is_empty() {
None
} else {
Some(&self[gen_index::<_, N>(rng, N)])
}
}
}
struct DisplayAsHexDigits<'a>(&'a [u8]);
impl<'a> std::fmt::Display for DisplayAsHexDigits<'a> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
use std::fmt::Write;
if self.0.is_empty() {
return f.write_str("[]");
}
f.write_str("[ ")?;
for b in self.0 {
match hex_digit(*b) {
d @ 0..=9 => f.write_char(('0' as u8 + d) as char),
d @ 10..=15 => f.write_char(('a' as u8 + d - 10) as char),
_ => write!(f, "0x{:x}", b),
}?;
f.write_char(' ')?;
}
f.write_char(']')?;
Ok(())
}
}
pub fn bench_1_6m(c: &mut Criterion) {
const LEN: usize = 1_600_000;
const LEN2: usize = LEN * 2;
let mut hex_bytes: Box<[MaybeUninit<u8>; LEN2]> =
unsafe { std::mem::transmute(Box::<[u8; LEN2]>::new_uninit()) };
let mut rng = rand::thread_rng();
for b in hex_bytes.iter_mut() {
*b = MaybeUninit::new(*HEX_CHARS.choose(&mut rng).unwrap());
}
let hex_bytes: Box<[u8; LEN2]> = unsafe { std::mem::transmute(hex_bytes) };
let bytes = match hex_bytes_dyn(hex_bytes.as_ref()) {
Some(b) => b,
None => {
panic!(
"Generated hex bytes were invalid: {}",
DisplayAsHexDigits(hex_bytes.as_ref())
);
}
};
test_sized::<LEN, true>(&hex_bytes, bytes.as_ref().try_into().unwrap());
benchmark_sized::<LEN, true>("[1.6m]", &hex_bytes, c);
}
criterion_group!(benches, bench_16, bench_256, bench_1_6m);
criterion_main!(benches);

View File

@ -1,7 +1,13 @@
#![feature(array_chunks)]
#![feature(const_slice_index)]
#![feature(extend_one)]
#![feature(generic_const_exprs)]
#![feature(int_log)]
#![feature(maybe_uninit_slice)]
#![feature(maybe_uninit_uninit_array)]
#![feature(maybe_uninit_array_assume_init)]
#![feature(const_maybe_uninit_array_assume_init)]
#![feature(const_maybe_uninit_uninit_array)]
#![feature(new_uninit)]
use std::fmt;
@ -15,9 +21,9 @@ const fn __make_ascii_digit_table() -> [u8; 256] {
const DIGIT_MIN: u8 = '0' as u8;
const DIGIT_MAX: u8 = '9' as u8;
const LOWER_MIN: u8 = 'a' as u8;
const LOWER_MAX: u8 = 'z' as u8;
const LOWER_MAX: u8 = 'f' as u8;
const UPPER_MIN: u8 = 'A' as u8;
const UPPER_MAX: u8 = 'Z' as u8;
const UPPER_MAX: u8 = 'F' as u8;
digits[i as usize] = match i {
DIGIT_MIN..=DIGIT_MAX => i - DIGIT_MIN,
@ -40,9 +46,9 @@ pub const fn hex_digit(ascii: u8) -> u8 {
// const DIGIT_MIN: u8 = '0' as u8;
// const DIGIT_MAX: u8 = '9' as u8;
// const LOWER_MIN: u8 = 'a' as u8;
// const LOWER_MAX: u8 = 'z' as u8;
// const LOWER_MAX: u8 = 'f' as u8;
// const UPPER_MIN: u8 = 'A' as u8;
// const UPPER_MAX: u8 = 'Z' as u8;
// const UPPER_MAX: u8 = 'F' as u8;
// match ascii {
// DIGIT_MIN..=DIGIT_MAX => ascii - DIGIT_MIN,
// LOWER_MIN..=LOWER_MAX => 10 + ascii - LOWER_MIN,
@ -81,18 +87,117 @@ pub const fn hex_byte_niched(msb: u8, lsb: u8) -> u16 {
(msb << 4) | (lsb & 0xf) | ((lsb & 0xf0) << 8)
}
#[inline]
pub const fn hex_bytes<const N: usize>(ascii: &[u8; N * 2]) -> Option<[u8; N]> {
let mut bytes = [0u8; N];
#[inline(always)]
const fn align_down_to<const N: usize>(n: usize) -> usize {
let shift = match N.checked_ilog2() {
Some(x) => x,
None => 0,
};
return n >> shift << shift;
}
#[inline(always)]
const fn align_up_to<const N: usize>(n: usize) -> usize {
let shift = match N.checked_ilog2() {
Some(x) => x,
None => 0,
};
return (n + (N - 1)) >> shift << shift;
}
#[inline(always)]
fn decode_hex_bytes_unchecked(ascii: &[u8], bytes: &mut [MaybeUninit<u8>]) -> bool {
let mut i = 0;
// use the maximum batch size that would be supported by AVX-512
const BATCH_SIZE: usize = 512 / 16;
const VECTORED: bool = false;
if VECTORED {
while i < align_down_to::<BATCH_SIZE>(bytes.len()) {
let mut buf = MaybeUninit::<u16>::uninit_array::<BATCH_SIZE>();
let mut j = 0;
while j < buf.len() {
*unsafe { buf.get_unchecked_mut(j) } = MaybeUninit::new(hex_byte_niched(
unsafe { *ascii.get_unchecked((i + j) << 1) },
unsafe { *ascii.get_unchecked(((i + j) << 1) + 1) },
));
j += 1;
}
let buf = unsafe { MaybeUninit::array_assume_init(buf) };
for x in buf.iter() {
if *x > u8::MAX as u16 {
return false;
}
}
let mut j = 0;
while j < buf.len() {
unsafe {
*bytes.get_unchecked_mut(i + j) = MaybeUninit::new(*buf.get_unchecked(j) as u8)
};
j += 1;
}
i += buf.len();
}
}
while i < bytes.len() {
bytes[i] = match hex_byte(ascii[i], ascii[i + 1]) {
Some(b) => b,
None => return None,
};
match hex_byte(unsafe { *ascii.get_unchecked(i << 1) }, unsafe {
*ascii.get_unchecked((i << 1) + 1)
}) {
Some(b) => unsafe { *bytes.get_unchecked_mut(i) = MaybeUninit::new(b) },
None => return false,
}
i += 1;
}
Some(bytes)
true
}
/// Use of this function should be restricted to `const` contexts because it is not vectorized like
/// the non-`const` alternative.
#[inline]
pub const fn hex_bytes_sized_const<const N: usize>(ascii: &[u8; N * 2]) -> Option<[u8; N]> {
if N == 0 {
Some([0u8; N])
} else {
let mut bytes = MaybeUninit::<u8>::uninit_array::<N>();
let mut i = 0;
while i < bytes.len() {
match hex_byte(unsafe { *ascii.get_unchecked(i << 1) }, unsafe {
*ascii.get_unchecked((i << 1) + 1)
}) {
Some(b) => bytes[i] = MaybeUninit::new(b),
None => return None,
}
i += 1;
}
Some(unsafe { MaybeUninit::array_assume_init(bytes) })
}
}
#[inline]
pub fn hex_bytes_sized<const N: usize>(ascii: &[u8; N * 2]) -> Option<[u8; N]> {
if N == 0 {
Some([0u8; N])
} else {
let mut bytes = MaybeUninit::<u8>::uninit_array::<N>();
if decode_hex_bytes_unchecked(ascii, &mut bytes) {
Some(unsafe { MaybeUninit::array_assume_init(bytes) })
} else {
None
}
}
}
pub fn hex_bytes_sized_heap<const N: usize>(ascii: &[u8; N * 2]) -> Option<Box<[u8; N]>> {
if N == 0 {
Some(Box::new([0u8; N]))
} else {
let mut bytes: Box<[MaybeUninit<u8>; N]> =
unsafe { std::mem::transmute(Box::<[u8; N]>::new_uninit()) };
if decode_hex_bytes_unchecked(ascii, bytes.as_mut()) {
Some(unsafe { std::mem::transmute(bytes) })
} else {
None
}
}
}
pub fn hex_bytes_dyn_unsafe(ascii: &[u8]) -> Option<Box<[u8]>> {
@ -103,7 +208,9 @@ pub fn hex_bytes_dyn_unsafe(ascii: &[u8]) -> Option<Box<[u8]>> {
let mut bytes = Box::<[u8]>::new_uninit_slice(len);
let mut i = 0;
while i < bytes.len() {
match hex_byte(unsafe { *ascii.get_unchecked(i) }, unsafe { *ascii.get_unchecked(i + 1) }) {
match hex_byte(unsafe { *ascii.get_unchecked(i << 1) }, unsafe {
*ascii.get_unchecked((i << 1) + 1)
}) {
Some(b) => bytes[i] = MaybeUninit::new(b),
None => return None,
}
@ -112,36 +219,23 @@ pub fn hex_bytes_dyn_unsafe(ascii: &[u8]) -> Option<Box<[u8]>> {
Some(unsafe { std::mem::transmute(bytes) })
}
pub fn hex_bytes_dyn_unsafe_for(ascii: &[u8]) -> Option<Box<[u8]>> {
let len = ascii.len() >> 1;
if len << 1 != ascii.len() {
return None;
}
let mut bytes = Box::<[u8]>::new_uninit_slice(len);
for i in 0..bytes.len() {
match hex_byte(unsafe { *ascii.get_unchecked(i) }, unsafe { *ascii.get_unchecked(i + 1) }) {
Some(b) => bytes[i] = MaybeUninit::new(b),
None => return None,
}
}
Some(unsafe { std::mem::transmute(bytes) })
}
pub fn hex_bytes_dyn_unsafe_iter(ascii: &[u8]) -> Option<Box<[u8]>> {
let len = ascii.len() >> 1;
if len << 1 != ascii.len() {
return None;
}
let mut bytes = Box::<[u8]>::new_uninit_slice(len);
for (i, o) in ascii.array_chunks::<2>()
for (i, o) in ascii
.array_chunks::<2>()
.map(|[msb, lsb]| hex_byte(*msb, *lsb))
.enumerate() {
if let Some(b) = o {
unsafe { *bytes.get_unchecked_mut(i) = MaybeUninit::new(b) };
} else {
return None;
}
.enumerate()
{
if let Some(b) = o {
unsafe { *bytes.get_unchecked_mut(i) = MaybeUninit::new(b) };
} else {
return None;
}
}
Some(unsafe { std::mem::transmute(bytes) })
}
@ -161,16 +255,17 @@ pub fn hex_bytes_dyn(ascii: &[u8]) -> Option<Box<[u8]>> {
if iter.remainder().len() != 0 {
return None;
}
iter
.map(|[msb, lsb]| hex_byte(*msb, *lsb))
iter.map(|[msb, lsb]| hex_byte(*msb, *lsb))
.collect::<Option<Vec<u8>>>()
.map(|v| v.into_boxed_slice())
}
struct ExtendRef<'a, T>(&'a mut T);
impl<'a, T, A> Extend<A> for ExtendRef<'a, T> where T: Extend<A> {
impl<'a, T, A> Extend<A> for ExtendRef<'a, T>
where
T: Extend<A>,
{
#[inline(always)]
fn extend<I: IntoIterator<Item = A>>(&mut self, iter: I) {
self.0.extend(iter)
@ -192,8 +287,7 @@ pub fn hex_bytes_dyn_niched(ascii: &[u8]) -> Option<Box<[u8]>> {
if iter.remainder().len() != 0 {
return None;
}
iter
.map(|[msb, lsb]| hex_byte_niched(*msb, *lsb))
iter.map(|[msb, lsb]| hex_byte_niched(*msb, *lsb))
.map(std::convert::TryFrom::try_from)
.map(Result::ok)
.collect::<Option<Vec<u8>>>()
@ -204,18 +298,65 @@ pub fn hex_bytes_dyn_niched(ascii: &[u8]) -> Option<Box<[u8]>> {
mod test {
use super::*;
const ASCII_BYTES: &[u8] = b"Donald J. Trump!";
const BYTES: &[u8] = b"Donald J. Trump!";
const HEX_BYTES: &[u8] = b"446F6E616C64204A2E205472756D7021";
#[test]
fn test_hex_digit() {
const HEX_DIGITS_LOWER: &[char; 16] = &[
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f',
];
const HEX_DIGITS_UPPER: &[char; 16] = &[
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F',
];
for (i, digit) in HEX_DIGITS_LOWER.into_iter().enumerate() {
assert_eq!(hex_digit(*digit as u8), i as u8);
}
}
#[test]
fn test_hex_byte() {
const HEX_BYTES_VALID: &[([u8; 2], u8)] = &[
(['f' as u8, 'f' as u8], 0xff),
(['0' as u8, '0' as u8], 0x00),
(['1' as u8, '1' as u8], 0x11),
(['e' as u8, 'f' as u8], 0xef),
(['f' as u8, 'e' as u8], 0xfe),
(['0' as u8, 'f' as u8], 0x0f),
(['f' as u8, '0' as u8], 0xf0),
];
for (hb, b) in HEX_BYTES_VALID {
assert_eq!(hex_byte(hb[0], hb[1]), Some(*b));
assert_eq!(hex_byte_niched(hb[0], hb[1]), *b as u16);
}
const HEX_BYTES_INVALID: &[[u8; 2]] = &[
['f' as u8, 'g' as u8],
['0' as u8, 'g' as u8],
['1' as u8, 'g' as u8],
['e' as u8, 'g' as u8],
['f' as u8, 'g' as u8],
['0' as u8, 'g' as u8],
['f' as u8, 'g' as u8],
];
for hb in HEX_BYTES_INVALID {
assert_eq!(hex_byte(hb[0], hb[1]), None);
assert!(hex_byte_niched(hb[0], hb[1]) & 0xff_00 != 0);
}
}
#[test]
fn test_non_niched() {
let result = hex_bytes_dyn(HEX_BYTES);
assert_eq!(Some(ASCII_BYTES), result.as_ref().map(Box::as_ref));
assert_eq!(Some(BYTES), result.as_ref().map(Box::as_ref));
}
#[test]
fn test_niched() {
let result = hex_bytes_dyn_niched(HEX_BYTES);
assert_eq!(Some(ASCII_BYTES), result.as_ref().map(Box::as_ref));
assert_eq!(Some(BYTES), result.as_ref().map(Box::as_ref));
}
}