Skip to content

Commit

Permalink
perf: less alloc in deserialization of HipByt and HipPath
Browse files Browse the repository at this point in the history
also cleanup serde for HipStr
  • Loading branch information
polazarus committed Dec 30, 2024
1 parent d61b71d commit 1eb22eb
Show file tree
Hide file tree
Showing 8 changed files with 266 additions and 77 deletions.
10 changes: 2 additions & 8 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@ all-features = true

[features]
default = ["std"]
std = ["serde/std", "serde_bytes/std"]
std = ["serde/std"]
unstable = []
serde = ["dep:serde", "dep:serde_bytes"]
serde = ["dep:serde"]
bstr = ["dep:bstr"]

[dev-dependencies]
Expand All @@ -44,12 +44,6 @@ optional = true
default-features = false
features = ["alloc"]

[dependencies.serde_bytes]
version = "0.11.3"
optional = true
default-features = false
features = ["alloc"]

[target.'cfg(loom)'.dependencies]
loom = "0.7"

Expand Down
166 changes: 161 additions & 5 deletions src/bytes/serde.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,48 @@
//! `serde` support for `HipByt`.
//!
//! This module provides support for serializing and deserializing `HipByt`
//! using [`serde`]. It is enabled by default when the `serde` feature is
//! enabled.
//!
//! # Examples
//!
//! ```
//! use hipstr::HipByt;
//!
//! let s = HipByt::from(b"hello");
//! let serialized = serde_json::to_string(&s).unwrap();
//! assert_eq!(serialized, "[104,101,108,108,111]");
//!
//! let deserialized: HipByt = serde_json::from_str(r#""hello""#).unwrap();
//! assert_eq!(deserialized, s);
//! ```
//!
//! # Notable aspects of the implementations
//!
//! Unlike the `Vec<T>` generic implementation which treats data as a generic
//! sequence, `HipByt` uses the more efficient byte sequence specific API for
//! serialization (similar to the [`serde_bytes`] crate). Note that the actual
//! outcome of a serialization depends on the underlying format's support for
//! byte sequences.
//!
//! During deserialization, this implementation minimizes allocations by reusing
//! the deserializer's internal buffer if possible.
//!
//! [`serde_bytes`]: https://docs.rs/serde_bytes
use core::marker::PhantomData;

use serde::de::Visitor;
use serde::{Deserialize, Serialize};

use super::HipByt;
use crate::alloc::borrow::Cow;
use crate::alloc::fmt;
use crate::alloc::string::String;
use crate::alloc::vec::Vec;
use crate::Backend;

const EXPECTING: &str = "a byte array";

impl<B> Serialize for HipByt<'_, B>
where
B: Backend,
Expand All @@ -17,6 +55,59 @@ where
}
}

/// Deserializer's visitor for owned `HipByt`.
struct OwnedVisitor<'borrow, B: Backend>(PhantomData<HipByt<'borrow, B>>);

impl<'de, 'borrow, B: Backend> Visitor<'de> for OwnedVisitor<'borrow, B> {
type Value = HipByt<'borrow, B>;

fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str(EXPECTING)
}

fn visit_bytes<E>(self, v: &[u8]) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(HipByt::from(v))
}

fn visit_byte_buf<E>(self, v: Vec<u8>) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(HipByt::from(v))
}

fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(HipByt::from(v.as_bytes()))
}

fn visit_string<E>(self, v: String) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(HipByt::from(v.into_bytes()))
}

fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
where
A: serde::de::SeqAccess<'de>,
{
let len = core::cmp::min(seq.size_hint().unwrap_or(0), 4096);
let mut bytes = Vec::with_capacity(len);

while let Some(b) = seq.next_element()? {
bytes.push(b);
}

Ok(HipByt::from(bytes))
}
}

impl<'de, B> Deserialize<'de> for HipByt<'_, B>
where
B: Backend,
Expand All @@ -25,8 +116,74 @@ where
where
D: serde::Deserializer<'de>,
{
let v: Vec<u8> = serde_bytes::deserialize(deserializer)?;
Ok(Self::from(v))
deserializer.deserialize_bytes(OwnedVisitor(PhantomData))
}
}

/// Deserializer's visitor for borrowed `HipByt`.
struct BorrowedVisitor<'de, B: Backend>(PhantomData<HipByt<'de, B>>);

impl<'de, B: Backend> Visitor<'de> for BorrowedVisitor<'de, B> {
type Value = HipByt<'de, B>;

fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str(EXPECTING)
}

fn visit_borrowed_bytes<E>(self, v: &'de [u8]) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(HipByt::borrowed(v))
}

fn visit_bytes<E>(self, v: &[u8]) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(HipByt::from(v))
}

fn visit_byte_buf<E>(self, v: Vec<u8>) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(HipByt::from(v))
}

fn visit_borrowed_str<E>(self, v: &'de str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(HipByt::borrowed(v.as_bytes()))
}

fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(HipByt::from(v.as_bytes()))
}

fn visit_string<E>(self, v: String) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(HipByt::from(v.into_bytes()))
}

fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
where
A: serde::de::SeqAccess<'de>,
{
let len = core::cmp::min(seq.size_hint().unwrap_or(0), 4096);
let mut bytes = Vec::with_capacity(len);

while let Some(b) = seq.next_element()? {
bytes.push(b);
}

Ok(HipByt::from(bytes))
}
}

Expand Down Expand Up @@ -62,8 +219,7 @@ where
D: serde::Deserializer<'de>,
B: Backend,
{
let cow: Cow<'de, [u8]> = serde_bytes::Deserialize::deserialize(deserializer)?;
Ok(HipByt::from(cow))
deserializer.deserialize_bytes(BorrowedVisitor(PhantomData))
}

#[cfg(test)]
Expand Down
2 changes: 1 addition & 1 deletion src/bytes/serde/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ fn test_serde() {
fn test_de_error() {
assert_de_tokens_error::<HipByt>(
&[Token::Bool(true)],
"invalid type: boolean `true`, expected byte array",
"invalid type: boolean `true`, expected a byte array",
);
}

Expand Down
2 changes: 1 addition & 1 deletion src/os_string.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ mod convert;

// OsStr(ing) implements Serialize/Deserialize only on Unix and Windows. thx @dsherret
#[cfg(all(feature = "serde", any(unix, windows)))]
mod serde;
pub mod serde;

#[cfg(test)]
mod tests;
Expand Down
24 changes: 24 additions & 0 deletions src/os_string/serde.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,27 @@
//! `serde` support for `HipOsStr`.
//!
//! This module provides support for serializing and deserializing `HipStr`
//! using [`serde`]. It is enabled by default when the `serde` feature is
//! enabled and on supported platforms (`unix` and `windows`).
//!
//! # Examples
//!
//! ```
//! use hipstr::HipStr;
//!
//! let s = HipStr::from("hello");
//! let serialized = serde_json::to_string(&s).unwrap();
//! assert_eq!(serialized, r#""hello""#);
//!
//! let deserialized: HipStr = serde_json::from_str(&serialized).unwrap();
//! assert_eq!(deserialized, s);
//! ```
//!
//! # Notable aspects of the implementation
//!
//! Due to the overall weirdness of `OsString` and their support in `serde`, no
//! attempt is made to improve on `OsString` standard `serde` implementation.
use std::ffi::OsString;

use serde::{Deserialize, Serialize};
Expand Down
71 changes: 32 additions & 39 deletions src/path/serde.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,35 @@
use std::path::PathBuf;
//! `serde` support for `HipPath`.
//!
//! This module provides support for serializing and deserializing `HipStr`
//! using [`serde`]. It is enabled by default when the `serde` feature is
//! enabled.
//!
//! # Examples
//!
//! ```
//! use hipstr::HipPath;
//!
//! let s = HipPath::borrowed("/usr/bin");
//! let serialized = serde_json::to_string(&s).unwrap();
//! assert_eq!(serialized, r#""/usr/bin""#);
//!
//! let deserialized: HipPath = serde_json::from_str(&serialized).unwrap();
//! assert_eq!(deserialized, s);
//! ```
//!
//! # Notable aspects of the implementation
//!
//! During deserialization, this implementation minimizes allocations by reusing
//! the deserializer's internal buffer if possible.
//!
//! Unlike `PathBuf`'s `Deserialize`, this implementation declares transparently
//! that it's expecting a string. Indeed not reusing `HipStr`'s implementation
//! just does not make any sense.
use serde::{de, Deserialize, Serialize};
use serde::{Deserialize, Serialize};

use super::HipPath;
use crate::alloc::borrow::Cow;
use crate::alloc::fmt;
use crate::string::HipStr;
use crate::Backend;

impl<B> Serialize for HipPath<'_, B>
Expand All @@ -29,39 +54,8 @@ where
where
D: serde::Deserializer<'de>,
{
Ok(Self::from(PathBuf::deserialize(deserializer)?))
}
}

/// Minimal string cow visitor
struct CowVisitor;

impl<'de> de::Visitor<'de> for CowVisitor {
type Value = Cow<'de, str>;

fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("path string")
}

fn visit_borrowed_str<E>(self, v: &'de str) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(Cow::Borrowed(v))
}

fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(Cow::Owned(v.to_string()))
}

fn visit_string<E>(self, v: String) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(Cow::Owned(v))
let s = HipStr::deserialize(deserializer)?;
Ok(Self::from(s))
}
}

Expand Down Expand Up @@ -96,8 +90,7 @@ where
D: serde::Deserializer<'de>,
B: Backend,
{
let cow: Cow<'de, str> = deserializer.deserialize_str(CowVisitor)?;
Ok(HipPath::from(cow))
crate::string::serde::borrow_deserialize(deserializer).map(HipPath::from)
}

#[cfg(test)]
Expand Down
4 changes: 2 additions & 2 deletions src/path/serde/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ fn test_serde() {
fn test_serde_err() {
assert_de_tokens_error::<HipPath>(
&[Token::I32(0)],
"invalid type: integer `0`, expected path string",
"invalid type: integer `0`, expected a string",
);
}

Expand Down Expand Up @@ -94,6 +94,6 @@ fn test_serde_borrow_err() {
Token::I32(0),
Token::StructEnd,
],
"invalid type: integer `0`, expected path string",
"invalid type: integer `0`, expected a string",
);
}
Loading

0 comments on commit 1eb22eb

Please sign in to comment.