Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a performant way to (de-)serialize fixed-size byte arrays #127

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
16 changes: 16 additions & 0 deletions Cargo.toml
Expand Up @@ -92,5 +92,21 @@ use-crc = ["crc", "paste"]
# NOT subject to SemVer guarantees!
experimental-derive = ["postcard-derive", "const_format"]

[dev-dependencies.criterion]
version = "0.5"

[dev-dependencies.serde-big-array]
version = "0.5.1"

[dev-dependencies.serde_bytes]
version = "0.11.12"

[dev-dependencies.serde-byte-array]
version = "0.1.2"

[[bench]]
name = "byte_array"
harness = false

[workspace]
members = ["postcard-derive"]
105 changes: 105 additions & 0 deletions benches/byte_array.rs
@@ -0,0 +1,105 @@
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use postcard::FixedSizeByteArray;
use serde::{Deserialize, Serialize};
use serde_byte_array::ByteArray;
use serde_bytes::{Bytes, ByteBuf};

fn serialize<const N: usize, const B: usize>(c: &mut Criterion)
where
[u8; N]: Serialize
{
let own: &_ = &FixedSizeByteArray::from([0; N]);
let bytes: &_ = Bytes::new(&[0; N]);
let byte_array: &_ = &ByteArray::new([0; N]);
let big_array: &_ = &serde_big_array::Array([0; N]);
let fixed: &_ = &[0; N];
let variable: &[u8] = &[0; N];
let mut buf = [0; B];
let mut group = c.benchmark_group(format!("serialize{}", N));
group.bench_function("own", |b| b.iter(|| {
let _ = black_box(postcard::to_slice(black_box(own), &mut buf).unwrap());
}));
group.bench_function("bytes", |b| b.iter(|| {
let _ = black_box(postcard::to_slice(black_box(bytes), &mut buf).unwrap());
}));
group.bench_function("byte_array", |b| b.iter(|| {
let _ = black_box(postcard::to_slice(black_box(byte_array), &mut buf).unwrap());
}));
group.bench_function("big_array", |b| b.iter(|| {
let _ = black_box(postcard::to_slice(black_box(big_array), &mut buf).unwrap());
}));
group.bench_function("fixed_size", |b| b.iter(|| {
let _ = black_box(postcard::to_slice(black_box(fixed), &mut buf).unwrap());
}));
group.bench_function("variable_size", |b| b.iter(|| {
let _ = black_box(postcard::to_slice(black_box(variable), &mut buf).unwrap());
}));
group.finish();
}

fn deserialize<const N: usize, const SN: usize>(c: &mut Criterion)
where
[u8; N]: Deserialize<'static>
{
let data = &[0; N];
let mut data_prefixed = [0; SN];
data_prefixed[0] = N as u8;
let data_prefixed = &data_prefixed;

let mut group = c.benchmark_group(format!("deserialize{}", N));
group.bench_function("own", |b| b.iter(|| {
let _: FixedSizeByteArray<N> = black_box(postcard::from_bytes(black_box(data)).unwrap());
}));
group.bench_function("bytes", |b| b.iter(|| {
let _: ByteBuf = black_box(postcard::from_bytes(black_box(data_prefixed)).unwrap());
}));
group.bench_function("byte_array", |b| b.iter(|| {
let _: ByteArray<N> = black_box(postcard::from_bytes(black_box(data_prefixed)).unwrap());
}));
group.bench_function("big_array", |b| b.iter(|| {
let _: serde_big_array::Array<u8, N> = black_box(postcard::from_bytes(black_box(data)).unwrap());
}));
group.bench_function("fixed_size", |b| b.iter(|| {
let _: [u8; N] = black_box(postcard::from_bytes(black_box(data)).unwrap());
}));
group.bench_function("variable_size", |b| b.iter(|| {
let _: Vec<u8> = black_box(postcard::from_bytes(black_box(data_prefixed)).unwrap());
}));
group.finish();
}

fn serialize0(c: &mut Criterion) { serialize::<0, 64>(c) }
fn serialize1(c: &mut Criterion) { serialize::<1, 64>(c) }
fn serialize2(c: &mut Criterion) { serialize::<2, 64>(c) }
fn serialize4(c: &mut Criterion) { serialize::<4, 64>(c) }
fn serialize8(c: &mut Criterion) { serialize::<8, 64>(c) }
fn serialize16(c: &mut Criterion) { serialize::<16, 64>(c) }
fn serialize32(c: &mut Criterion) { serialize::<32, 64>(c) }

fn deserialize0(c: &mut Criterion) { deserialize::<0, 1>(c) }
fn deserialize1(c: &mut Criterion) { deserialize::<1, 2>(c) }
fn deserialize2(c: &mut Criterion) { deserialize::<2, 3>(c) }
fn deserialize4(c: &mut Criterion) { deserialize::<4, 5>(c) }
fn deserialize8(c: &mut Criterion) { deserialize::<8, 9>(c) }
fn deserialize16(c: &mut Criterion) { deserialize::<16, 17>(c) }
fn deserialize32(c: &mut Criterion) { deserialize::<32, 33>(c) }

criterion_group!(byte_array,
serialize0,
serialize1,
serialize2,
serialize4,
serialize8,
serialize16,
serialize32,

deserialize0,
deserialize1,
deserialize2,
deserialize4,
deserialize8,
deserialize16,
deserialize32,
);

criterion_main!(byte_array);
142 changes: 142 additions & 0 deletions src/byte_array.rs
@@ -0,0 +1,142 @@
use serde::ser::SerializeTupleStruct;
use serde::Serialize;
use serde::Serializer;

use core::convert::TryInto;
use core::fmt;
use serde::Deserializer;
use serde::Deserialize;
use serde::de;

/// Represents a fixed-size byte array for better performance with `postcard`.
///
/// This struct *only* works with `postcard` (de-)serialization.
#[repr(transparent)]
pub struct FixedSizeByteArray<const N: usize> {
inner: FixedSizeByteArrayInner<N>,
}

impl<const N: usize> From<[u8; N]> for FixedSizeByteArray<N> {
fn from(array: [u8; N]) -> FixedSizeByteArray<N> {
FixedSizeByteArray {
inner: FixedSizeByteArrayInner {
array,
},
}
}
}

impl<const N: usize> FixedSizeByteArray<N> {
/// Extract the actual array.
pub fn into_inner(self) -> [u8; N] {
self.inner.array
}
}

#[repr(transparent)]
struct FixedSizeByteArrayInner<const N: usize> {
array: [u8; N],
}

pub static TOKEN: &str = "$postcard::private::FixedSizeByteArray";

impl<const N: usize> Serialize for FixedSizeByteArray<N> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut s = serializer.serialize_tuple_struct(TOKEN, 1)?;
s.serialize_field(&self.inner)?;
s.end()
}
}

impl<const N: usize> Serialize for FixedSizeByteArrayInner<N> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_bytes(&self.array)
}
}

impl<'de, const N: usize> Deserialize<'de> for FixedSizeByteArray<N> {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
struct Visitor<const N: usize>;

impl<'de, const N: usize> de::Visitor<'de> for Visitor<N> {
type Value = FixedSizeByteArray<N>;

fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
write!(formatter, "byte array of length {}", N)
}

fn visit_bytes<E>(self, v: &[u8]) -> Result<Self::Value, E>
where
E: de::Error,
{
let array: [u8; N] = match v.try_into() {
Ok(a) => a,
Err(_) => return Err(de::Error::invalid_length(v.len(), &self)),
};
Ok(FixedSizeByteArray::from(array))
}
}

deserializer.deserialize_tuple_struct(TOKEN, N, Visitor)
}
}

#[cfg(test)]
mod tests {
use crate::Error;
use super::FixedSizeByteArray;

#[test]
fn test_byte_array_serialize() {
let empty = FixedSizeByteArray::from([]);
let mut buf = [0; 32];
let serialized = crate::to_slice(&empty, &mut buf).unwrap();
assert_eq!(serialized, &[]);

let single = FixedSizeByteArray::from([0x12]);
let mut buf = [0; 32];
let serialized = crate::to_slice(&single, &mut buf).unwrap();
assert_eq!(serialized, &[0x12]);

let five_bytes = FixedSizeByteArray::from([0x12, 0x34, 0x56, 0x78, 0x90]);
let mut buf = [0; 32];
let serialized = crate::to_slice(&five_bytes, &mut buf).unwrap();
assert_eq!(serialized, &[0x12, 0x34, 0x56, 0x78, 0x90]);
}

#[test]
fn test_byte_array_deserialize() {
let deserialized: FixedSizeByteArray<0> = crate::from_bytes(&[]).unwrap();
assert_eq!(deserialized.into_inner(), []);

let deserialized: FixedSizeByteArray<0> = crate::from_bytes(&[0x12]).unwrap();
assert_eq!(deserialized.into_inner(), []);

let deserialized: FixedSizeByteArray<1> = crate::from_bytes(&[0x12]).unwrap();
assert_eq!(deserialized.into_inner(), [0x12]);

let deserialized: FixedSizeByteArray<5> = crate::from_bytes(&[0x12, 0x34, 0x56, 0x78, 0x90]).unwrap();
assert_eq!(deserialized.into_inner(), [0x12, 0x34, 0x56, 0x78, 0x90]);
}

#[test]
fn test_byte_array_deserialize_error() {
let result: Result<FixedSizeByteArray<1>, _> = crate::from_bytes(&[]);
assert_eq!(result.err().unwrap(), Error::DeserializeUnexpectedEnd);

let result: Result<FixedSizeByteArray<8>, _> = crate::from_bytes(&[0x12]);
assert_eq!(result.err().unwrap(), Error::DeserializeUnexpectedEnd);

let result: Result<FixedSizeByteArray<8>, _> = crate::from_bytes(&[0x12, 0x34, 0x56, 0x78]);
assert_eq!(result.err().unwrap(), Error::DeserializeUnexpectedEnd);
}
}
9 changes: 7 additions & 2 deletions src/de/deserializer.rs
Expand Up @@ -473,14 +473,19 @@ impl<'de, 'a, F: Flavor<'de>> de::Deserializer<'de> for &'a mut Deserializer<'de
#[inline]
fn deserialize_tuple_struct<V>(
self,
_name: &'static str,
name: &'static str,
len: usize,
visitor: V,
) -> Result<V::Value>
where
V: Visitor<'de>,
{
self.deserialize_tuple(len, visitor)
if name.as_ptr() == crate::byte_array::TOKEN.as_ptr() {
jamesmunns marked this conversation as resolved.
Show resolved Hide resolved
let bytes: &'de [u8] = self.flavor.try_take_n(len)?;
visitor.visit_borrowed_bytes(bytes)
} else {
self.deserialize_tuple(len, visitor)
}
}

#[inline]
Expand Down
2 changes: 2 additions & 0 deletions src/lib.rs
Expand Up @@ -4,6 +4,7 @@
#![cfg_attr(doc_cfg, feature(doc_cfg))]

pub mod accumulator;
mod byte_array;
mod de;

mod eio;
Expand Down Expand Up @@ -81,6 +82,7 @@ pub mod experimental {
}
}

pub use byte_array::FixedSizeByteArray;
pub use de::deserializer::Deserializer;
pub use de::flavors as de_flavors;
pub use de::{from_bytes, from_bytes_cobs, take_from_bytes, take_from_bytes_cobs};
Expand Down