Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,16 @@ arbitrary = { version = "1.0", features = ["derive"], optional = true }
itertools = "0.14.0"

[dev-dependencies]
criterion = "0.7.0"
ethereum_ssz_derive = "0.9.0"
serde_json = "1.0.0"
tree_hash_derive = "0.10.0"

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

# FIXME: remove before release
[patch.crates-io]
ethereum_ssz = { git = "https://github.com/sigp/ethereum_ssz", branch = "main" }
ethereum_ssz_derive = { git = "https://github.com/sigp/ethereum_ssz", branch = "main" }
118 changes: 118 additions & 0 deletions benches/encode_decode.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
use criterion::{criterion_group, criterion_main, Criterion};
use ssz::{Decode, DecodeError, Encode};
use ssz_types::{FixedVector, VariableList};
use std::hint::black_box;
use std::time::Duration;
use typenum::{Unsigned, U1048576, U131072};

#[derive(Clone, Debug, Default, PartialEq, Eq, ssz_derive::Encode)]
#[ssz(struct_behaviour = "transparent")]
pub struct ByteVector<N: Unsigned>(FixedVector<u8, N>);

impl<N: Unsigned> ssz::Decode for ByteVector<N> {
fn is_ssz_fixed_len() -> bool {
true
}

fn from_ssz_bytes(bytes: &[u8]) -> Result<Self, DecodeError> {
FixedVector::new(bytes.to_vec())
.map(Self)
.map_err(|e| DecodeError::BytesInvalid(format!("{e:?}")))
}

fn ssz_fixed_len() -> usize {
<FixedVector<u8, N> as ssz::Decode>::ssz_fixed_len()
}
}

fn benchmark_fixed_vector(c: &mut Criterion) {
let mut group = c.benchmark_group("fixed_vector");

let fixed_vector_u8 = FixedVector::<u8, U1048576>::new(vec![255u8; 1048576]).unwrap();
let fixed_vector_u64 = FixedVector::<u64, U131072>::new(vec![255u64; 131072]).unwrap();
let fixed_vector_bytes = fixed_vector_u8.as_ssz_bytes();

group.warm_up_time(Duration::from_secs(15));
group.measurement_time(Duration::from_secs(10));

group.bench_function("decode_byte_u8_1m", |b| {
b.iter(|| {
let vector = ByteVector::<U1048576>::from_ssz_bytes(&fixed_vector_bytes).unwrap();
black_box(vector);
});
});

group.bench_function("decode_u8_1m", |b| {
b.iter(|| {
let vector = FixedVector::<u8, U1048576>::from_ssz_bytes(&fixed_vector_bytes).unwrap();
black_box(vector);
});
});

group.bench_function("encode_u8_1m", |b| {
b.iter(|| {
let bytes = fixed_vector_u8.as_ssz_bytes();
black_box(bytes);
});
});

group.bench_function("decode_u64_128k", |b| {
b.iter(|| {
let vector = FixedVector::<u64, U131072>::from_ssz_bytes(&fixed_vector_bytes).unwrap();
black_box(vector);
});
});
group.bench_function("encode_u64_128k", |b| {
b.iter(|| {
let bytes = fixed_vector_u64.as_ssz_bytes();
black_box(bytes);
});
});

group.finish();
}

fn benchmark_variable_list(c: &mut Criterion) {
let mut group = c.benchmark_group("variable_list");

let variable_list_u8 = VariableList::<u8, U1048576>::new(vec![255u8; 1048576]).unwrap();
let variable_list_u64 = VariableList::<u64, U131072>::new(vec![255u64; 131072]).unwrap();
let variable_list_bytes = variable_list_u8.as_ssz_bytes();

group.warm_up_time(Duration::from_secs(15));
group.measurement_time(Duration::from_secs(10));

group.bench_function("decode_u8_1m", |b| {
b.iter(|| {
let vector =
VariableList::<u8, U1048576>::from_ssz_bytes(&variable_list_bytes).unwrap();
black_box(vector);
});
});

group.bench_function("encode_u8_1m", |b| {
b.iter(|| {
let bytes = variable_list_u8.as_ssz_bytes();
black_box(bytes);
});
});

group.bench_function("decode_u64_128k", |b| {
b.iter(|| {
let vector =
VariableList::<u64, U131072>::from_ssz_bytes(&variable_list_bytes).unwrap();
black_box(vector);
});
});
group.bench_function("encode_u64_128k", |b| {
b.iter(|| {
let bytes = variable_list_u64.as_ssz_bytes();
black_box(bytes);
});
});

group.finish();
}

criterion_group!(benches, benchmark_fixed_vector, benchmark_variable_list);
criterion_main!(benches);
47 changes: 40 additions & 7 deletions src/fixed_vector.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use crate::Error;
use serde::Deserialize;
use serde_derive::Serialize;
use std::marker::PhantomData;
use std::mem;
use std::ops::{Deref, DerefMut, Index, IndexMut};
use std::slice::SliceIndex;
use tree_hash::Hash256;
Expand Down Expand Up @@ -275,6 +276,13 @@ impl<T, N: Unsigned> ssz::TryFromIter<T> for FixedVector<T, N> {
}
}

#[inline(always)]
pub fn from_ssz_bytes_u8_only<N: Unsigned>(
bytes: &[u8],
) -> Result<FixedVector<u8, N>, ssz::DecodeError> {
Ok(FixedVector::new(bytes.to_vec()).unwrap())
}

impl<T, N: Unsigned> ssz::Decode for FixedVector<T, N>
where
T: ssz::Decode,
Expand All @@ -299,6 +307,25 @@ where
len: 0,
expected: 1,
})
} else if mem::size_of::<T>() == 1 && mem::align_of::<T>() == 1 {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using size_of and align_of removes the need to add any bounds on T. Other crates like bytemuck would have required us to constrain T quite a lot, which sort of defeats the point of a generic impl.

Even using TypeId would have required us to add 'static.

The other advantage of this is that it works for types that encode as u8, like the ParticipationFlags in the BeaconState.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

couldn't T be a bool as well?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I guess it could be, but we never use bools in any consensus data structures, do we?

Would be a good test case

if bytes.len() != fixed_len {
return Err(ssz::DecodeError::BytesInvalid(format!(
"FixedVector of {} items has {} items",
fixed_len,
bytes.len(),
)));
}

// Safety: We've verified T is u8, so Vec<T> is Vec<u8>
// and bytes.to_vec() produces Vec<u8>
let vec_u8 = bytes.to_vec();
let vec_t = unsafe { std::mem::transmute::<Vec<u8>, Vec<T>>(vec_u8) };
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should add a test for a type that is not u8

Self::new(vec_t).map_err(|e| {
ssz::DecodeError::BytesInvalid(format!(
"Wrong number of FixedVector elements: {:?}",
e
))
})
} else if T::is_ssz_fixed_len() {
let num_items = bytes
.len()
Expand All @@ -312,13 +339,19 @@ where
)));
}

let vec = bytes.chunks(T::ssz_fixed_len()).try_fold(
Vec::with_capacity(num_items),
|mut vec, chunk| {
vec.push(T::from_ssz_bytes(chunk)?);
Ok(vec)
},
)?;
if bytes.len() != num_items * T::ssz_fixed_len() {
return Err(ssz::DecodeError::BytesInvalid(format!(
"FixedVector of {} items has {} bytes",
num_items,
bytes.len()
)));
}

let mut vec = Vec::with_capacity(num_items);
for chunk in bytes.chunks_exact(T::ssz_fixed_len()) {
vec.push(T::from_ssz_bytes(chunk)?);
}

Self::new(vec).map_err(|e| {
ssz::DecodeError::BytesInvalid(format!(
"Wrong number of FixedVector elements: {:?}",
Expand Down
2 changes: 1 addition & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
//! ```
#[macro_use]
mod fixed_vector;
pub mod fixed_vector;
pub mod serde_utils;
mod tree_hash;
mod variable_list;
Expand Down
37 changes: 30 additions & 7 deletions src/variable_list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,26 @@ where
return Ok(Self::default());
}

if std::mem::size_of::<T>() == 1 && std::mem::align_of::<T>() == 1 {
if bytes.len() > max_len {
return Err(ssz::DecodeError::BytesInvalid(format!(
"VariableList of {} items exceeds maximum of {}",
bytes.len(),
max_len
)));
}

// Safety: We've verified T has the same memory layout as u8, so Vec<T> *is* Vec<u8>.
let vec_u8 = bytes.to_vec();
let vec_t = unsafe { std::mem::transmute::<Vec<u8>, Vec<T>>(vec_u8) };
return Self::new(vec_t).map_err(|e| {
ssz::DecodeError::BytesInvalid(format!(
"Wrong number of VariableList elements: {:?}",
e
))
});
}

if T::is_ssz_fixed_len() {
let num_items = bytes
.len()
Expand All @@ -298,13 +318,16 @@ where
)));
}

bytes.chunks(T::ssz_fixed_len()).try_fold(
Vec::with_capacity(num_items),
|mut vec, chunk| {
vec.push(T::from_ssz_bytes(chunk)?);
Ok(vec)
},
)
let mut vec = Vec::with_capacity(num_items);
for chunk in bytes.chunks_exact(T::ssz_fixed_len()) {
vec.push(T::from_ssz_bytes(chunk)?);
}
Self::new(vec).map_err(|e| {
ssz::DecodeError::BytesInvalid(format!(
"Wrong number of VariableList elements: {:?}",
e
))
})
} else {
ssz::decode_list_of_variable_length_items(bytes, Some(max_len))
}?
Expand Down
Loading