//! SIMD-accelerated hex encoding. use std::mem::MaybeUninit; use std::simd::*; use crate::prelude::*; const REQUIRED_ALIGNMENT: usize = 64; pub const HEX_CHARS_LOWER: [u8; 16] = array_op!(map[16, ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f']] |_, c| c as u8); pub const HEX_CHARS_UPPER: [u8; 16] = array_op!(map[16, HEX_CHARS_LOWER] |_, c| (c as char).to_ascii_uppercase() as u8); const __HEX_CHARS_LOWER_SIMD: [u32; 16] = util::cast_u8_u32(HEX_CHARS_LOWER); const HEX_CHARS_LOWER_SIMD: *const i32 = &__HEX_CHARS_LOWER_SIMD as *const u32 as *const i32; const __HEX_CHARS_UPPER_SIMD: [u32; 16] = util::cast_u8_u32(HEX_CHARS_UPPER); const HEX_CHARS_UPPER_SIMD: *const i32 = &__HEX_CHARS_UPPER_SIMD as *const u32 as *const i32; // TODO: add a check for endianness (current is assumed LE) const HEX_BYTES_LOWER: [u16; 256] = array_op!(gen[256] |i| ((HEX_CHARS_LOWER[(i & 0xf0) >> 4] as u16)) | ((HEX_CHARS_LOWER[i & 0x0f] as u16) << 8)); const HEX_BYTES_UPPER: [u16; 256] = array_op!(gen[256] |i| ((HEX_CHARS_UPPER[(i & 0xf0) >> 4] as u16)) | ((HEX_CHARS_UPPER[i & 0x0f] as u16) << 8)); macro_rules! select { ($cond:ident ? $true:ident : $false:ident) => { if $cond { $true } else { $false } }; (($cond:expr) ? ($true:expr) : ($false:expr)) => { if $cond { $true } else { $false } }; } const HEX_CHARS_LOWER_VEC128: arch::__m128i = unsafe { util::cast(HEX_CHARS_LOWER) }; const HEX_CHARS_UPPER_VEC128: arch::__m128i = unsafe { util::cast(HEX_CHARS_UPPER) }; const HEX_CHARS_LOWER_VEC256: arch::__m256i = unsafe { util::cast([HEX_CHARS_LOWER, HEX_CHARS_LOWER]) }; const HEX_CHARS_UPPER_VEC256: arch::__m256i = unsafe { util::cast([HEX_CHARS_UPPER, HEX_CHARS_UPPER]) }; #[inline(always)] const fn nbl_to_ascii(nbl: u8) -> u8 { // fourth bit set if true let at_least_10 = { // 10: 1010 // 11: 1011 // 12: 1100 // 13: 1101 // 14: 1110 // 15: 1111 //let b1 = nbl & 0b1010; //let b2 = nbl & 0b1100; //((nbl >> 1) | (b2 & (b2 << 1)) | (b1 & (b1 << 2))) & 0b1000 //((b2 & (b2 << 1)) | (b1 & (b1 << 2))) & 0b1000 if nbl >= 10 { 0b1 } else { 0b0 } }; // 6th bit is always 1 with a-z and 0-9 let b6_val = if UPPER { (at_least_10 ^ 0b1) << 5 } else { 0b100000 }; // 5th bit is always 1 with 0-9 let b5_val = (at_least_10 ^ 0b1) << 4; // 7th bit is always 1 with a-z and A-Z let b7_val = at_least_10 << 6; // fill all bits with the value of the 4th bit let is_at_least_10_all_mask = (((at_least_10 << 7) as i8) >> 7) as u8; // sub 9 if we're >=10 // 9: 1001 // a-z and A-Z start at ..0001 rather than ..0000 like 0-9, so we sub 9, not 10 let sub = 9 & is_at_least_10_all_mask; // interestingly, this is much slower than the nastly alt we're using above //let sub = at_least_10 | (at_least_10 >> 3); // apply the sub, then OR in the constants (nbl - sub) | b6_val | b5_val | b7_val } #[inline(always)] const fn nbl_wide_to_ascii(nbl: u16) -> u16 { // fourth bit set if true let at_least_10 = { let b1 = nbl & 0b1010; let b2 = nbl & 0b1100; //((nbl >> 1) | (b2 & (b2 << 1)) | (b1 & (b1 << 2))) & 0b1000 ((b2 & (b2 << 1)) | (b1 & (b1 << 2))) & 0b1000 }; // mask used don the 6th bit. let b6_val = if UPPER { (at_least_10 ^ 0b1000) << 2 } else { 0b100000 }; let b5_val = (at_least_10 ^ 0b1000) << 1; let b7_val = at_least_10 << 3; // sign extend the 1 if set let is_at_least_10_all_mask = (((at_least_10 << 12) as i16) >> 15) as u16; let sub = 9 & is_at_least_10_all_mask; let c = (nbl - sub) | b6_val | b5_val | b7_val; c } // the way this is used, is by inserting the u16 directly into a byte array, so on a little-endian system (assumed in the code), we need the low byte shifted to the left, which seems counterintuitive. #[inline(always)] const fn byte_to_ascii(byte: u8) -> u16 { //let byte = byte as u16; //nbl_wide_to_ascii::((byte & 0xf0) >> 4) | (nbl_wide_to_ascii::(byte & 0x0f) << 8) (nbl_to_ascii::((byte & 0xf0) >> 4) as u16) | ((nbl_to_ascii::(byte & 0x0f) as u16) << 8) } macro_rules! const_impl1 { ($UPPER:ident, $src:ident, $dst:ident) => {{ let mut i = 0; const UNROLL: usize = 8; let ub = $dst.len(); let aub = util::align_down_to::<{ 2 * UNROLL }>(ub); let mut src = $src.as_ptr(); let mut dst = $dst.as_mut_ptr() as *mut u8; while i < aub { unsafe { let [b1, b2, b3, b4, b5, b6, b7, b8] = [(); UNROLL]; unroll!(let [b1, b2, b3, b4, b5, b6, b7, b8] => |_| { let b = *src; src = src.add(1); b }); unroll!([(0, b1), (2, b2), (4, b3), (6, b4), (8, b5), (10, b6), (12, b7), (14, b8)] => |i, b| { *dst.add(i) = *select!($UPPER ? HEX_CHARS_UPPER : HEX_CHARS_LOWER).get_unchecked((b >> 4) as usize) }); unroll!([(0, b1), (2, b2), (4, b3), (6, b4), (8, b5), (10, b6), (12, b7), (14, b8)] => |i, b| { *dst.add(i + 1) = *select!($UPPER ? HEX_CHARS_UPPER : HEX_CHARS_LOWER).get_unchecked((b & 0x0f) as usize) }); dst = dst.add(2 * UNROLL); i += 2 * UNROLL; } } while i < ub { unsafe { let b = *src; *dst = *select!($UPPER ? HEX_CHARS_UPPER : HEX_CHARS_LOWER).get_unchecked((b >> 4) as usize); dst = dst.add(1); *dst = *select!($UPPER ? HEX_CHARS_UPPER : HEX_CHARS_LOWER).get_unchecked((b & 0x0f) as usize); dst = dst.add(1); i += 2; src = src.add(1); } } }}; } #[inline(always)] fn u64_to_ne_u16(v: u64) -> [u16; 4] { unsafe { std::mem::transmute(v.to_ne_bytes()) } } macro_rules! const_impl { ($UPPER:ident, $src:ident, $dst:ident) => {{ let mut i = 0; const UNROLL: usize = 8; let ub = $src.len(); let aub = util::align_down_to::<{ UNROLL }>(ub); let mut src = $src.as_ptr() as *const u8; //let mut dst = $dst.as_mut_ptr() as *mut u64; let mut dst = $dst.as_mut_ptr() as *mut u16; // 2-8% slower on 256-bytes input const USE_LOOKUP_TABLE: bool = false; while i < aub { unsafe { // benchmarks show this to be 40% faster than a u64 unaligned_read().to_ne_bytes() let [b1, b2, b3, b4, b5, b6, b7, b8] = [(); UNROLL]; unroll!(let [b1, b2, b3, b4, b5, b6, b7, b8] => |_| { let b = *src; src = src.add(1); b }); unroll!(let [b1, b2, b3, b4, b5, b6, b7, b8] => |b| { if USE_LOOKUP_TABLE { *select!($UPPER ? HEX_BYTES_UPPER : HEX_BYTES_LOWER).get_unchecked(b as usize) } else { byte_to_ascii::<$UPPER>(b) } }); /*unroll!(let [b1: (0, b1), b2: (1, b2), b3: (2, b3), b4: (3, b4), b5: (4, b5), b6: (5, b6), b7: (6, b7), b8: (7, b8)] => |j, v| { if j < 4 { (v as u64) << (j * 16) } else { (v as u64) << ((j - 4) * 16) } });*/ // TODO: would using vector store actually be faster here (particularly for the // heap variant) unroll!([(0, b1), (1, b2), (2, b3), (3, b4), (4, b5), (5, b6), (6, b7), (7, b8)] => |_, v| { //*dst = *select!($UPPER ? HEX_BYTES_UPPER : HEX_BYTES_LOWER).get_unchecked(b as usize); *dst = v; dst = dst.add(1); }); /*let mut buf1: u64 = 0; let mut buf2: u64 = 0; unroll!([(0, b1), (1, b2), (2, b3), (3, b4), (4, b5), (5, b6), (6, b7), (7, b8)] => |j, v| { if j < 4 { //println!("[{j}] {v:064b}"); buf1 |= v; } else { //println!("[{j}] {v:064b}"); buf2 |= v; } // if i < 4 { // buf1[i] = MaybeUninit::new(v); // } else { // buf2[i - 4] = MaybeUninit::new(v); // } }); //assert!(dst < ($dst.as_mut_ptr() as *mut u64).add($dst.len())); *dst = buf1; dst = dst.add(1); //assert!(dst < ($dst.as_mut_ptr() as *mut u64).add($dst.len())); *dst = buf2; dst = dst.add(1);*/ i += UNROLL; } } let mut dst = dst as *mut u16; while i < ub { unsafe { let b = *src; *dst = if USE_LOOKUP_TABLE { *select!($UPPER ? HEX_BYTES_UPPER : HEX_BYTES_LOWER).get_unchecked(b as usize) } else { byte_to_ascii::<$UPPER>(b) }; dst = dst.add(1); src = src.add(1); i += 1; } } }}; } /// The `$dst` must be 32-byte aligned. macro_rules! common_impl { (@disabled $UPPER:ident, $src:ident, $dst:ident) => { const_impl!($UPPER, $src, $dst) }; ($UPPER:ident, $src:ident, $dst:ident) => {{ let mut i = 0; let ub = $dst.len(); let aub = util::align_down_to::(ub); let mut src = $src.as_ptr(); let mut dst = $dst.as_mut_ptr(); let hi_mask = 0xf0u8.splat().into(); let lo_mask = 0x0fu8.splat().into(); /*{ let aligned = unsafe { src.add(src.align_offset(128 / 8)) }; while src < aligned { unsafe { let b = *src; *dst = MaybeUninit::new(*select!($UPPER ? HEX_CHARS_UPPER : HEX_CHARS_LOWER).get_unchecked((b >> 4) as usize)); dst = dst.add(1); *dst = MaybeUninit::new(*select!($UPPER ? HEX_CHARS_UPPER : HEX_CHARS_LOWER).get_unchecked((b & 0x0f) as usize)); dst = dst.add(1); i += 2; src = src.add(1); } } } let aub = i + util::align_down_to::(ub - i); { let x = src as usize; assert_eq!((aub - i) % DIGIT_BATCH_SIZE, 0, "aub is missized"); assert_eq!(util::align_down_to::<{ 128 / 8 }>(x), x, "src ptr is misaligned"); }*/ while i < aub { unsafe { //let hi_los = $src.as_ptr().add(i) as *const [u8; GATHER_BATCH_SIZE]; //let chunk = $src.as_ptr().add(i >> 1) as *const [u8; WIDE_BATCH_SIZE]; //let chunk = *chunk; //let chunk: simd::arch::__m128i = Simd::from_array(chunk).into(); let chunk: simd::arch::__m128i; // We've aligned the src ptr std::arch::asm!("vmovdqu {dst}, [{src}]", src = in(reg) src, dst = lateout(xmm_reg) chunk, options(pure, readonly, preserves_flags, nostack)); let hi = chunk.and(hi_mask); // 64 vs 16 seems to make no difference let hi: simd::arch::__m128i = simd::shr!(64, 4, (xmm_reg) hi); let lo = chunk.and(lo_mask); //unroll!(let [hi, lo] => |x| Simd::::from(x)); if_trace_simd! { unroll!(let [hi, lo] => |x| Simd::::from(x)); println!("hi,lo: {hi:02x?}, {lo:02x?}"); } unroll!(let [hi, lo] => |x| simd::arch::_mm_shuffle_epi8(select!($UPPER ? HEX_CHARS_UPPER_VEC128 : HEX_CHARS_LOWER_VEC128), x)); if_trace_simd! { unroll!(let [hi, lo] => |x| Simd::::from(x)); println!("hi: {hi:02x?}"); println!("lo: {lo:02x?}"); } let interleaved = simd::interleave_m128(hi, lo); if_trace_simd! { unroll!(let [spaced_hi, spaced_lo] => |x| Simd::::from(x)); println!("INTERLEAVE_HI: {INTERLEAVE_HI:02x?}"); println!("INTERLEAVE_LO: {INTERLEAVE_LO:02x?}"); println!("spaced_hi: {spaced_hi:02x?}"); println!("spaced_lo: {spaced_lo:02x?}"); } if_trace_simd! { let interleaved: Simd = interleaved.into(); println!("interleaved: {interleaved:x?}"); } core::arch::asm!("vmovdqa [{}], {}", in(reg) dst as *mut i8, in(ymm_reg) interleaved, options(preserves_flags, nostack)); dst = dst.add(DIGIT_BATCH_SIZE); i += DIGIT_BATCH_SIZE; src = src.add(WIDE_BATCH_SIZE); } } while i < ub { unsafe { let b = *src; *dst = MaybeUninit::new(*select!($UPPER ? HEX_CHARS_UPPER : HEX_CHARS_LOWER).get_unchecked((b >> 4) as usize)); dst = dst.add(1); *dst = MaybeUninit::new(*select!($UPPER ? HEX_CHARS_UPPER : HEX_CHARS_LOWER).get_unchecked((b & 0x0f) as usize)); dst = dst.add(1); i += 2; src = src.add(1); } } if_trace_simd! { let slice: &[_] = $dst.as_ref(); match std::str::from_utf8(unsafe { &*(slice as *const [_] as *const [u8]) }) { Ok(s) => { println!("encoded: {s:?}"); } Err(e) => { println!("encoded corrupted utf8: {e}"); } } } }}; } macro_rules! define_encode { ($name:ident$(<$N:ident>)?($in:ty) { str => $str:ident, write => $write:ident $(,)? } $(where $( $where:tt )+)?) => { fn $str$()?(src: $in) -> String $( where $( $where )+ )?; fn $write$()?(w: impl std::fmt::Write, src: $in) -> std::fmt::Result $( where $( $where )+ )?; }; } macro_rules! impl_encode_str { ($name:ident$(<$N:ident>)?($in:ty) => $impl:ident (|$bytes:ident| $into_vec:expr) $(where $( $where:tt )+)?) => { #[inline] fn $name$()?(src: $in) -> String $( where $( $where )+ )? { let $bytes = Self::$impl(src); unsafe { String::from_utf8_unchecked($into_vec) } } }; } macro_rules! impl_encode_write { ($name:ident$(<$N:ident>)?($in:ty) => $impl:ident (|$bytes:ident| $into_slice:expr) $(where $( $where:tt )+)?) => { #[inline] fn $name$()?(mut w: impl std::fmt::Write, src: $in) -> std::fmt::Result $( where $( $where )+ )? { let $bytes = Self::$impl(src); let s = unsafe { std::str::from_utf8_unchecked($into_slice) }; w.write_str(s) } }; } pub struct DisplaySized<'a, E: Encode + ?Sized, const N: usize>(&'a [u8; N], std::marker::PhantomData); pub struct DisplaySizedHeap<'a, E: Encode + ?Sized, const N: usize>(&'a [u8; N], std::marker::PhantomData); pub struct DisplaySlice<'a, E: Encode + ?Sized>(&'a [u8], std::marker::PhantomData); impl<'a, E: Encode, const N: usize> std::fmt::Display for DisplaySized<'a, E, N> where [u8; N * 2]: { #[inline(always)] fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { E::write_sized(f, self.0) } } impl<'a, E: Encode, const N: usize> std::fmt::Display for DisplaySizedHeap<'a, E, N> where [u8; N * 2]: { #[inline(always)] fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { E::write_sized_heap(f, self.0) } } impl<'a, E: Encode> std::fmt::Display for DisplaySlice<'a, E> { #[inline(always)] fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { E::write_slice(f, self.0) } } pub trait Encode { /// Encodes the sized input on the stack. fn enc_sized(src: &[u8; N]) -> [u8; N * 2] where [u8; N * 2]:; /// Encodes the sized input on the heap. fn enc_sized_heap(src: &[u8; N]) -> Box<[u8; N * 2]> where [u8; N * 2]:; /// Encodes the unsized input on the heap. fn enc_slice(src: &[u8]) -> Box<[u8]>; define_encode!(enc_sized(&[u8; N]) { str => enc_str_sized, write => write_sized, } where [u8; N * 2]:); define_encode!(enc_sized_heap(&[u8; N]) { str => enc_str_sized_heap, write => write_sized_heap, } where [u8; N * 2]:); define_encode!(enc_slice(&[u8]) { str => enc_str_slice, write => write_slice, }); /// Returns an `impl Display` of the sized input on the stack. fn display_sized(src: &[u8; N]) -> DisplaySized<'_, Self, N> where [u8; N * 2]:; /// Returns an `impl Display` of the sized input on the heap. fn display_sized_heap(src: &[u8; N]) -> DisplaySizedHeap<'_, Self, N> where [u8; N * 2]:; /// Returns an `impl Display` of the unsized input on the heap. fn display_slice(src: &[u8]) -> DisplaySlice<'_, Self>; } pub struct Encoder; pub struct Buffer(MaybeUninit<[u8; N * 2]>) where [u8; N * 2]:; impl Buffer where [u8; N * 2]: { #[inline] pub fn new() -> Self { Self(MaybeUninit::uninit()) } #[inline] pub fn format_exact(&mut self, bytes: &[u8; N]) -> &str { self.0 = MaybeUninit::new(Encoder::::enc_sized(bytes)); unsafe { std::str::from_utf8_unchecked(self.0.assume_init_ref()) } } // TODO: support using only part of the buffer. /*pub fn format(&mut self, bytes: &[u8]) -> &str { assert!(bytes.len() <= N); self.0 = MaybeUninit::new(Encoder::::enc_slice(bytes)); unsafe { std::str::from_utf8_unchecked(self.0.assume_init_ref()) } }*/ } #[repr(align(32))] struct Aligned32(T); impl Encode for Encoder { #[inline] fn enc_sized(src: &[u8; N]) -> [u8; N * 2] where [u8; N * 2]:, { // SAFETY: `Aligned32` has no initialization in and of itself, nor does an array of `MaybeUninit` let mut buf = unsafe { MaybeUninit::; N * 2]>>::uninit().assume_init() }; let buf1 = &mut buf.0; common_impl!(UPPER, src, buf1); unsafe { MaybeUninit::array_assume_init(buf.0) } } #[inline] fn enc_sized_heap(src: &[u8; N]) -> Box<[u8; N * 2]> where [u8; N * 2]:, { let mut buf: Box<[MaybeUninit; N * 2]> = unsafe { util::alloc_aligned_box::<_, REQUIRED_ALIGNMENT>() }; common_impl!(UPPER, src, buf); unsafe { Box::from_raw(Box::into_raw(buf).cast()) } } #[inline] fn enc_slice(src: &[u8]) -> Box<[u8]> { let mut buf: Box<[MaybeUninit]> = unsafe { util::alloc_aligned_box_slice::<_, REQUIRED_ALIGNMENT>(src.len() * 2) }; common_impl!(UPPER, src, buf); unsafe { Box::<[_]>::assume_init(buf) } } impl_encode_str!(enc_str_sized(&[u8; N]) => enc_sized (|bytes| bytes.into()) where [u8; N * 2]:); impl_encode_str!(enc_str_sized_heap(&[u8; N]) => enc_sized_heap (|bytes| { Vec::from_raw_parts(Box::into_raw(bytes) as *mut u8, N * 2, N * 2) }) where [u8; N * 2]:); impl_encode_str!(enc_str_slice(&[u8]) => enc_slice (|bytes| Vec::from(bytes))); impl_encode_write!(write_sized(&[u8; N]) => enc_sized (|bytes| &bytes) where [u8; N * 2]:); impl_encode_write!(write_sized_heap(&[u8; N]) => enc_sized_heap (|bytes| bytes.as_ref()) where [u8; N * 2]:); impl_encode_write!(write_slice(&[u8]) => enc_slice (|bytes| bytes.as_ref())); #[inline(always)] fn display_sized(src: &[u8; N]) -> DisplaySized<'_, Self, N> where [u8; N * 2]:, { DisplaySized(src, std::marker::PhantomData) } #[inline(always)] fn display_sized_heap(src: &[u8; N]) -> DisplaySizedHeap<'_, Self, N> where [u8; N * 2]:, { DisplaySizedHeap(src, std::marker::PhantomData) } #[inline(always)] fn display_slice(src: &[u8]) -> DisplaySlice<'_, Self> { DisplaySlice(src, std::marker::PhantomData) } } impl Encoder { // TODO: mark this const when #![feature(const_mut_refs)] is stabilized #[inline] pub fn enc_const(src: &[u8; N]) -> [u8; N * 2] where [u8; N * 2]:, { let mut buf = MaybeUninit::uninit_array(); const_impl!(UPPER, src, buf); unsafe { MaybeUninit::array_assume_init(buf) } } } #[cfg(test)] mod test { use super::*; use crate::test::*; #[test] fn test_nbl_to_ascii() { for i in 0..16 { let a = nbl_to_ascii::(i); let b = HEX_CHARS_LOWER[i as usize]; assert_eq!(a, b, "({i}) {a:08b} != {b:08b}"); let a = nbl_to_ascii::(i); let b = HEX_CHARS_UPPER[i as usize]; assert_eq!(a, b, "({i}) {a:08b} != {b:08b}"); } } #[test] fn test_nbl_wide_to_ascii() { for i in 0..16 { let a = nbl_wide_to_ascii::(i); let b = HEX_CHARS_LOWER[i as usize] as u16; assert_eq!(a, b, "({i}) {a:08b} != {b:08b}"); let a = nbl_wide_to_ascii::(i); let b = HEX_CHARS_UPPER[i as usize] as u16; assert_eq!(a, b, "({i}) {a:08b} != {b:08b}"); } } #[test] fn test_byte_to_ascii() { for i in 0..=255 { let a = byte_to_ascii::(i); let b = HEX_BYTES_LOWER[i as usize]; assert_eq!(a, b, "({i}) {a:016b} != {b:016b}"); let a = byte_to_ascii::(i); let b = HEX_BYTES_UPPER[i as usize]; assert_eq!(a, b, "({i}) {a:016b} != {b:016b}"); } } macro_rules! for_each_sample { ($name:ident, |$ss:pat_param, $shs:pat_param, $sb:pat_param, $shb:pat_param| $expr:expr) => { #[test] fn $name() { let $ss = STR; let $shs = HEX_STR; let $sb = BYTES; let $shb = HEX_BYTES; $expr; let $ss = LONG_STR; let $shs = LONG_HEX_STR; let $sb = LONG_BYTES; let $shb = LONG_HEX_BYTES; $expr; } }; } type Enc = Encoder; for_each_sample!(enc_const, |_, _, b, hb| assert_eq!(Enc::enc_const(b), *hb)); for_each_sample!(enc_sized, |_, _, b, hb| assert_eq!(Enc::enc_sized(b), *hb)); for_each_sample!(enc_sized_heap, |_, _, b, hb| assert_eq!( Enc::enc_sized_heap(b), Box::new(*hb) )); for_each_sample!(enc_slice, |_, _, b, hb| assert_eq!( Enc::enc_slice(b), (*hb).into_iter().collect::>().into_boxed_slice() )); for_each_sample!(enc_str_sized, |_, hs, b, _| assert_eq!(Enc::enc_str_sized(b), hs.to_owned())); for_each_sample!(enc_str_sized_heap, |_, hs, b, _| assert_eq!( Enc::enc_str_sized_heap(b), hs.to_owned() )); for_each_sample!(enc_str_slice, |_, hs, b, _| assert_eq!( Enc::enc_str_slice(b), hs.to_owned() )); }