diff --git a/Cargo.lock b/Cargo.lock index abfac8f..4eb0a60 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -48,6 +48,7 @@ dependencies = [ "rand_chacha", "rust_decimal", "serde", + "smol_str", "time", "uuid", "zstd", @@ -62,12 +63,28 @@ dependencies = [ "syn", ] +[[package]] +name = "borsh" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfd1e3f8955a5d7de9fab72fc8373fade9fb8a703968cb200ae3dc6cf08e185a" +dependencies = [ + "bytes", + "cfg_aliases", +] + [[package]] name = "bytemuck" version = "1.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4" +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + [[package]] name = "cc" version = "1.2.51" @@ -86,6 +103,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + [[package]] name = "crc32fast" version = "1.5.0" @@ -332,6 +355,16 @@ version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" +[[package]] +name = "smol_str" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4aaa7368fcf4852a4c2dd92df0cace6a71f2091ca0a23391ce7f3a31833f1523" +dependencies = [ + "borsh", + "serde_core", +] + [[package]] name = "syn" version = "2.0.114" diff --git a/Cargo.toml b/Cargo.toml index 46cc43e..f27bd77 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,6 +21,7 @@ bytemuck = { version = "1.14", features = [ "min_const_generics", "must_cast" ] glam = { version = ">=0.21", default-features = false, optional = true } rust_decimal = { version = "1.36", default-features = false, optional = true } serde = { version = "1.0", default-features = false, features = [ "alloc" ], optional = true } +smol_str = { version = "0.3", default-features = false, optional = true } time = { version = "0.3", default-features = false, optional = true } uuid = { version = "1.10", default-features = false, optional = true } @@ -41,11 +42,11 @@ zstd = "0.13.0" [features] derive = [ "dep:bitcode_derive" ] -std = [ "serde?/std", "glam?/std", "arrayvec?/std" ] +std = [ "serde?/std", "glam?/std", "arrayvec?/std", "smol_str?/std" ] default = [ "derive", "std" ] [package.metadata.docs.rs] -features = [ "derive", "serde", "std" ] +features = [ "derive", "serde", "smol_str", "std" ] # TODO halfs speed of benches_borrowed::bench_bitcode_decode #[profile.bench] diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index 72805f1..3798894 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -10,10 +10,11 @@ cargo-fuzz = true [dependencies] arrayvec = { version = "0.7", features = ["serde"] } -bitcode = { path = "..", features = [ "arrayvec", "rust_decimal", "serde", "time" ] } +bitcode = { path = "..", features = [ "arrayvec", "rust_decimal", "serde", "smol_str", "time" ] } libfuzzer-sys = "0.4" rust_decimal = "1.36.0" serde = { version ="1.0", features = [ "derive" ] } +smol_str = { version = "0.3", features = ["serde"] } time = { version = "0.3", features = ["serde"]} # Prevent this from interfering with workspaces diff --git a/fuzz/fuzz_targets/fuzz.rs b/fuzz/fuzz_targets/fuzz.rs index a9beda4..5c377c7 100644 --- a/fuzz/fuzz_targets/fuzz.rs +++ b/fuzz/fuzz_targets/fuzz.rs @@ -11,6 +11,7 @@ use std::num::NonZeroU32; use std::time::Duration; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6}; use rust_decimal::Decimal; +use smol_str::SmolStr; #[inline(never)] fn test_derive(data: &[u8]) { @@ -233,5 +234,6 @@ fuzz_target!(|data: &[u8]| { SocketAddrV6, SocketAddr, time::Time, + SmolStr, ); }); diff --git a/src/derive/impls.rs b/src/derive/impls.rs index 61fd569..e35eceb 100644 --- a/src/derive/impls.rs +++ b/src/derive/impls.rs @@ -31,6 +31,8 @@ macro_rules! impl_both { impl_both!(bool, BoolEncoder, BoolDecoder); impl_both!(f32, F32Encoder, F32Decoder); impl_both!(String, StrEncoder, StrDecoder); +#[cfg(feature = "smol_str")] +impl_both!(smol_str::SmolStr, StrEncoder, StrDecoder); macro_rules! impl_int { ($($t:ty),+) => { diff --git a/src/str.rs b/src/str.rs index 80d727d..3ad30a1 100644 --- a/src/str.rs +++ b/src/str.rs @@ -5,7 +5,6 @@ use crate::error::err; use crate::fast::{NextUnchecked, SliceImpl}; use crate::length::LengthDecoder; use crate::u8_char::U8Char; -use alloc::borrow::ToOwned; use alloc::string::String; use alloc::vec::Vec; use core::num::NonZeroUsize; @@ -57,21 +56,37 @@ impl<'b> Encoder<&'b str> for StrEncoder { } } -impl Encoder for StrEncoder { - #[inline(always)] - fn encode(&mut self, t: &String) { - self.encode(t.as_str()); - } +macro_rules! impl_string { + ($t:ty) => { + impl crate::coder::Encoder<$t> for crate::str::StrEncoder { + #[inline(always)] + fn encode(&mut self, t: &$t) { + self.encode(t.as_str()); + } + + #[inline(always)] + fn encode_vectored<'a>(&mut self, i: impl Iterator + Clone) + where + $t: 'a, + { + self.encode_vectored(i.map(|s| s.as_str())); + } + } - #[inline(always)] - fn encode_vectored<'a>(&mut self, i: impl Iterator + Clone) - where - String: 'a, - { - self.encode_vectored(i.map(String::as_str)); - } + impl<'a> crate::coder::Decoder<'a, $t> for crate::str::StrDecoder<'a> { + #[inline(always)] + fn decode(&mut self) -> $t { + let v: &str = self.decode(); + <$t>::from(v) + } + } + }; } +impl_string!(String); +#[cfg(feature = "smol_str")] +impl_string!(smol_str::SmolStr); + // Doesn't use VecDecoder because can't decode &[u8]. #[derive(Default)] pub struct StrDecoder<'a> { @@ -131,14 +146,6 @@ impl<'a> Decoder<'a, &'a str> for StrDecoder<'a> { } } -impl<'a> Decoder<'a, String> for StrDecoder<'a> { - #[inline(always)] - fn decode(&mut self) -> String { - let v: &str = self.decode(); - v.to_owned() - } -} - /// Tests 128 bytes a time instead of `<[u8]>::is_ascii` which only tests 8. /// 390% faster on 8KB, 27% faster on 1GB (RAM bottleneck). fn is_ascii_simd(v: &[u8]) -> bool { @@ -288,3 +295,26 @@ mod tests2 { } crate::bench_encode_decode!(str_vec: Vec); } + +#[cfg(all(test, feature = "smol_str"))] +mod smol_str_tests { + use smol_str::SmolStr; + + /// Short strings stay inline after decode (no heap allocation). + #[test] + fn decoded_short_string_is_not_heap_allocated() { + let s = SmolStr::new("hello"); + assert!(!s.is_heap_allocated()); + let decoded: SmolStr = crate::decode(&crate::encode(&s)).unwrap(); + assert!(!decoded.is_heap_allocated()); + } + + /// Long strings are heap-allocated after decode. + #[test] + fn decoded_long_string_is_heap_allocated() { + let s = SmolStr::new("this is a longer string that exceeds inline storage"); + assert!(s.is_heap_allocated()); + let decoded: SmolStr = crate::decode(&crate::encode(&s)).unwrap(); + assert!(decoded.is_heap_allocated()); + } +}