From 17054126e8bf803f52ba44a89d54ebcf766e9344 Mon Sep 17 00:00:00 2001 From: "Tobin C. Harding" Date: Mon, 13 Apr 2026 16:32:16 +1000 Subject: [PATCH] Implement perf optimization using MaybeUninit Using a zero initialized array on the stack may be slower than using a `MaybeUninit` because of the required initialization to zero just to overwrite the buffer. However using (testing) `MaybeUninit` is a bit of a nuisance so leave the original function `drain_to_slice` in the codebase but cfg it out unless we are testing. --- src/iter.rs | 30 ++++++++++++++++++++++++++++-- src/lib.rs | 32 +++++++++++++++++++++++++++++--- 2 files changed, 57 insertions(+), 5 deletions(-) diff --git a/src/iter.rs b/src/iter.rs index b4692c6..6bf24de 100644 --- a/src/iter.rs +++ b/src/iter.rs @@ -5,6 +5,7 @@ use core::borrow::Borrow; use core::convert::TryInto; use core::iter::FusedIterator; +use core::mem::MaybeUninit; use core::str; #[cfg(feature = "std")] use std::io; @@ -47,14 +48,39 @@ impl<'a> HexToBytesIter> { Self::from_pairs(HexDigitsIter::new_unchecked(s.as_bytes())) } - /// Writes all the bytes yielded by this `HexToBytesIter` to the provided slice. + /// Writes all the bytes yielded by this `HexToBytesIter` to the provided uninitialized slice. /// - /// Stops writing if this `HexToBytesIter` yields an `InvalidCharError`. + /// Stops writing if this `HexToBytesIter` yields an `InvalidCharError`. On error, bytes + /// written before the error remain initialized in `buf`, but the caller cannot rely on + /// any particular prefix being initialized and must not treat `buf` as `&[u8]`. /// /// # Panics /// /// Panics if the length of this `HexToBytesIter` is not equal to the length of the provided /// slice. + pub(crate) fn drain_to_uninit_slice( + self, + buf: &mut [MaybeUninit], + ) -> Result<(), InvalidCharError> { + // IF YOU CHANGE THIS FUNCTION DO THE ONE BELOW TOO. + assert_eq!(self.len(), buf.len()); + let mut ptr = buf.as_mut_ptr().cast::(); + for byte in self { + // SAFETY: for loop iterates `len` times, and `buf` has length `len`. + // Writing through a `*mut u8` derived from `*mut MaybeUninit` is sound + // because the two have identical layout and `MaybeUninit` imposes no + // validity requirement on the bytes being overwritten. + unsafe { + core::ptr::write(ptr, byte?); + ptr = ptr.add(1); + } + } + Ok(()) + } + + // Exactly the same as the function above except without the `MaybeUinit` stuff. Used to test + // the function above. + #[cfg(test)] pub(crate) fn drain_to_slice(self, buf: &mut [u8]) -> Result<(), InvalidCharError> { assert_eq!(self.len(), buf.len()); let mut ptr = buf.as_mut_ptr(); diff --git a/src/lib.rs b/src/lib.rs index f30b588..ca6e375 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -97,6 +97,7 @@ pub mod prelude { #[cfg(feature = "alloc")] use alloc::vec::Vec; +use core::mem::MaybeUninit; pub(crate) use table::Table; @@ -137,10 +138,20 @@ pub fn decode_to_vec(hex: &str) -> Result, DecodeVariableLengthBytesErro /// `N * 2`.) pub fn decode_to_array(hex: &str) -> Result<[u8; N], DecodeFixedLengthBytesError> { if hex.len() == N * 2 { - let mut ret = [0u8; N]; + // SAFETY: `[MaybeUninit; N]` has no initialization requirement, + // so an uninitialized array of them is sound. This is the standard + // `uninit_array` pattern. + let mut ret: [MaybeUninit; N] = unsafe { MaybeUninit::uninit().assume_init() }; + // checked above - HexToBytesIter::new_unchecked(hex).drain_to_slice(&mut ret)?; - Ok(ret) + HexToBytesIter::new_unchecked(hex).drain_to_uninit_slice(&mut ret)?; + + // SAFETY: `drain_to_uninit_slice` returning `Ok` means all N bytes + // were written. `[MaybeUninit; N]` and `[u8; N]` have identical + // layout, so the transmute is sound. + #[allow(clippy::borrow_as_ptr)] + #[allow(clippy::ptr_as_ptr)] + Ok(unsafe { (&ret as *const _ as *const [u8; N]).read() }) } else { Err(InvalidLengthError { invalid: hex.len(), expected: 2 * N }.into()) } @@ -273,4 +284,19 @@ mod tests { assert_eq!(HASH[0], 0x00); assert_eq!(HASH[31], 0x6f); } + + // This implicitly test `drain_to_uninit_slice()`. + // In `iter::hex_to_bytes_slice_drain` we test `drain_to_slice()` + #[test] + fn decode_to_vec() { + let hex = "deadbeef"; + let want = [0xde, 0xad, 0xbe, 0xef]; + let got = crate::decode_to_vec(hex).unwrap(); + assert_eq!(got, want); + + let hex = ""; + let want: [u8; 0] = []; + let got = crate::decode_to_vec(hex).unwrap(); + assert_eq!(got, want); + } }