From 2bcb9661336e195e07d09fb0caab5555c5741d41 Mon Sep 17 00:00:00 2001 From: weekday Date: Fri, 10 Jan 2025 06:04:56 +0000 Subject: [PATCH 1/2] Add benchmarks for BlobSidecar serialization and deserialization Generalize some code in eth2_cache_utils and benches to work on collections containing any type. --- Cargo.lock | 1 + benches/Cargo.toml | 1 + benches/benches/types.rs | 114 +++++++++++++++++++++++--------- eth2_cache_utils/src/generic.rs | 24 +++---- eth2_cache_utils/src/lib.rs | 2 +- eth2_cache_utils/src/mainnet.rs | 10 ++- 6 files changed, 108 insertions(+), 44 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c59a3b56..2e668568 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -691,6 +691,7 @@ dependencies = [ "num-integer", "openssl", "operation_pools", + "serde", "serde_json", "serde_utils", "sha2 0.10.8", diff --git a/benches/Cargo.toml b/benches/Cargo.toml index a0fd97e2..bd96d7a9 100644 --- a/benches/Cargo.toml +++ b/benches/Cargo.toml @@ -29,6 +29,7 @@ itertools = { workspace = true } num-integer = { workspace = true } openssl = { workspace = true } operation_pools = { workspace = true } +serde = { workspace = true } serde_json = { workspace = true } serde_utils = { workspace = true } sha2 = { workspace = true } diff --git a/benches/benches/types.rs b/benches/benches/types.rs index 81817bde..206085bf 100644 --- a/benches/benches/types.rs +++ b/benches/benches/types.rs @@ -10,12 +10,16 @@ use std::sync::Arc; use allocator as _; use criterion::{Criterion, Throughput}; use easy_ext::ext; -use eth2_cache_utils::{goerli, mainnet, medalla, LazyBeaconBlocks, LazyBeaconState}; +use eth2_cache_utils::{ + goerli, mainnet, medalla, LazyBeaconBlocks, LazyBeaconState, LazyBlobSidecars, +}; use itertools::Itertools as _; -use ssz::{SszRead as _, SszWrite as _}; +use serde::{Deserialize, Serialize}; +use ssz::{SszRead, SszWrite}; use types::{ combined::{BeaconState, SignedBeaconBlock}, config::Config, + deneb::containers::BlobSidecar, preset::Preset, }; @@ -73,6 +77,10 @@ fn main() { &Config::medalla(), &medalla::BEACON_BLOCKS_UP_TO_SLOT_128, ) + .benchmark_blob_sidecars( + "mainnet Deneb blob sidecars from 32 slots", + &mainnet::DENEB_BLOB_SIDECARS_FROM_32_SLOTS, + ) .final_summary(); } @@ -120,35 +128,82 @@ impl Criterion { config: &Config, blocks: &LazyBeaconBlocks

, ) -> &mut Self { - let ssz_bytes = LazyCell::new(|| blocks_to_ssz(blocks.force())); - let json_bytes = LazyCell::new(|| blocks_to_json_directly(blocks.force())); + let ssz_bytes = LazyCell::new(|| slice_to_ssz(blocks.force())); + let json_bytes = LazyCell::new(|| slice_to_json_directly(blocks.force())); self.benchmark_group(group_name) .throughput(Throughput::Elements(blocks.count())) .bench_function("from SSZ", |bencher| { let ssz_bytes = ssz_bytes.iter().map(Vec::as_slice); - bencher.iter_with_large_drop(|| blocks_from_ssz::

(config, ssz_bytes.clone())) + bencher.iter_with_large_drop(|| { + vec_from_ssz::>>(config, ssz_bytes.clone()) + }) }) .bench_function("to SSZ", |bencher| { let blocks = blocks.force(); - bencher.iter_with_large_drop(|| blocks_to_ssz(blocks)) + bencher.iter_with_large_drop(|| slice_to_ssz(blocks)) }) .bench_function("from JSON", |bencher| { let json_bytes = json_bytes.iter().map(Vec::as_slice); - bencher.iter_with_large_drop(|| blocks_from_json::

(json_bytes.clone())) + bencher.iter_with_large_drop(|| { + vec_from_json::>>(json_bytes.clone()) + }) }) .bench_function("to JSON directly", |bencher| { let blocks = blocks.force(); - bencher.iter_with_large_drop(|| blocks_to_json_directly(blocks)) + bencher.iter_with_large_drop(|| slice_to_json_directly(blocks)) }) .bench_function("to JSON via serde_utils::stringify", |bencher| { let blocks = blocks.force(); - bencher.iter_with_large_drop(|| blocks_to_json_via_stringify(blocks)) + bencher.iter_with_large_drop(|| slice_to_json_via_stringify(blocks)) + }); + + self + } + + fn benchmark_blob_sidecars( + &mut self, + group_name: &str, + blob_sidecars: &LazyBlobSidecars

, + ) -> &mut Self { + let ssz_bytes = LazyCell::new(|| slice_to_ssz(blob_sidecars.force())); + let json_bytes = LazyCell::new(|| slice_to_json_directly(blob_sidecars.force())); + + self.benchmark_group(group_name) + .throughput(Throughput::Elements(blob_sidecars.count())) + .bench_function("from SSZ", |bencher| { + let ssz_bytes = ssz_bytes.iter().map(Vec::as_slice); + + bencher.iter_with_large_drop(|| { + vec_from_ssz::<(), Arc>>(&(), ssz_bytes.clone()) + }) + }) + .bench_function("to SSZ", |bencher| { + let blob_sidecars = blob_sidecars.force(); + + bencher.iter_with_large_drop(|| slice_to_ssz(blob_sidecars)) + }) + .bench_function("from JSON", |bencher| { + let json_bytes = json_bytes.iter().map(Vec::as_slice); + + bencher.iter_with_large_drop(|| { + vec_from_json::>>(json_bytes.clone()) + }) + }) + .bench_function("to JSON directly", |bencher| { + let blob_sidecars = blob_sidecars.force(); + + bencher.iter_with_large_drop(|| slice_to_json_directly(blob_sidecars)) + }) + .bench_function("to JSON via serde_utils::stringify", |bencher| { + let blob_sidecars = blob_sidecars.force(); + + bencher.iter_with_large_drop(|| slice_to_json_via_stringify(blob_sidecars)) }); self @@ -175,47 +230,46 @@ fn state_to_json_via_stringify(state: &BeaconState) -> Vec { .expect("state should be serializable to JSON") } -fn blocks_from_ssz<'bytes, P: Preset>( - config: &Config, +fn vec_from_ssz<'bytes, C, T: SszRead>( + context: &C, bytes: impl IntoIterator, -) -> Vec>> { +) -> Vec { bytes .into_iter() - .map(|bytes| Arc::from_ssz(config, bytes)) + .map(|bytes| T::from_ssz(context, bytes)) .try_collect() - .expect("blocks have already been successfully deserialized") + .expect("iterator items have already been successfully deserialized from SSZ") } -fn blocks_to_ssz(blocks: &[Arc>]) -> Vec> { - blocks - .iter() - .map(Arc::to_ssz) - .try_collect() - .expect("blocks can be serialized because they have already been serialized to a file") +fn slice_to_ssz(slice: &[impl SszWrite]) -> Vec> { + slice.iter().map(SszWrite::to_ssz).try_collect().expect( + "slice elements can be serialized to SSZ because \ + they have already been serialized to a file", + ) } -fn blocks_from_json<'bytes, P: Preset>( +fn vec_from_json<'bytes, T: Deserialize<'bytes>>( bytes: impl IntoIterator, -) -> Vec>> { +) -> Vec { bytes .into_iter() .map(serde_json::from_slice) .try_collect() - .expect("blocks should be deserializable from JSON") + .expect("iterator items should be deserializable from JSON") } -fn blocks_to_json_directly(blocks: &[Arc>]) -> Vec> { - blocks +fn slice_to_json_directly(slice: &[impl Serialize]) -> Vec> { + slice .iter() .map(serde_json::to_vec) .try_collect() - .expect("blocks should be serializable to JSON") + .expect("slice elements should be serializable to JSON") } -fn blocks_to_json_via_stringify(blocks: &[Arc>]) -> Vec> { - blocks +fn slice_to_json_via_stringify(slice: &[impl Serialize]) -> Vec> { + slice .iter() - .map(|block| serde_utils::stringify(block).and_then(|json| serde_json::to_vec(&json))) + .map(|element| serde_utils::stringify(element).and_then(|json| serde_json::to_vec(&json))) .try_collect() - .expect("blocks should be serializable to JSON") + .expect("slice elements should be serializable to JSON") } diff --git a/eth2_cache_utils/src/generic.rs b/eth2_cache_utils/src/generic.rs index e2799cab..15865f36 100644 --- a/eth2_cache_utils/src/generic.rs +++ b/eth2_cache_utils/src/generic.rs @@ -16,6 +16,9 @@ use types::{ traits::SignedBeaconBlock as _, }; +pub type LazyBeaconBlocks

= LazyVec>>; +pub type LazyBlobSidecars

= LazyVec>>; + // `LazyLock` implements `core::ops::Deref`, which is more confusing than useful. // Explicit forcing is better. @@ -51,20 +54,17 @@ impl LazyBeaconBlock

{ } } -pub struct LazyBeaconBlocks { +pub struct LazyVec { expected_count: u64, - blocks: LazyLock>>>, + elements: LazyLock>, } -impl LazyBeaconBlocks

{ +impl LazyVec { #[must_use] - pub(crate) const fn new( - expected_count: u64, - thunk: fn() -> Vec>>, - ) -> Self { + pub(crate) const fn new(expected_count: u64, thunk: fn() -> Vec) -> Self { Self { expected_count, - blocks: LazyLock::new(thunk), + elements: LazyLock::new(thunk), } } @@ -73,13 +73,13 @@ impl LazyBeaconBlocks

{ self.expected_count } - pub fn force(&self) -> &[Arc>] { - let blocks = LazyLock::force(&self.blocks); - let actual_count = u64::try_from(blocks.len()).expect("block count should fit in u64"); + pub fn force(&self) -> &[T] { + let elements = LazyLock::force(&self.elements); + let actual_count = u64::try_from(elements.len()).expect("count should fit in u64"); assert_eq!(actual_count, self.expected_count); - blocks + elements } } diff --git a/eth2_cache_utils/src/lib.rs b/eth2_cache_utils/src/lib.rs index 1ffe01a5..76b2a1ea 100644 --- a/eth2_cache_utils/src/lib.rs +++ b/eth2_cache_utils/src/lib.rs @@ -6,7 +6,7 @@ //! [`eth2-cache`]: ../../../eth2-cache/ //! [`predefined_chains`]: ../predefined_chains/index.html -pub use generic::{LazyBeaconBlock, LazyBeaconBlocks, LazyBeaconState}; +pub use generic::{LazyBeaconBlock, LazyBeaconBlocks, LazyBeaconState, LazyBlobSidecars}; pub mod goerli; pub mod holesky; diff --git a/eth2_cache_utils/src/mainnet.rs b/eth2_cache_utils/src/mainnet.rs index 325df07f..1fc16040 100644 --- a/eth2_cache_utils/src/mainnet.rs +++ b/eth2_cache_utils/src/mainnet.rs @@ -10,7 +10,7 @@ use types::{ preset::Mainnet, }; -use crate::generic::{self, LazyBeaconBlock, LazyBeaconBlocks, LazyBeaconState}; +use crate::generic::{self, LazyBeaconBlock, LazyBeaconBlocks, LazyBeaconState, LazyBlobSidecars}; const CASE: Case = Case { case_path_relative_to_workspace_root: "eth2-cache/mainnet", @@ -64,6 +64,14 @@ pub static ALTAIR_BEACON_BLOCKS_FROM_8192_SLOTS: LazyBeaconBlocks = pub static CAPELLA_BEACON_BLOCKS_FROM_244816_SLOTS: LazyBeaconBlocks = LazyBeaconBlocks::new(127, || beacon_blocks(7_834_112..=7_834_240, 7)); +pub static DENEB_BLOB_SIDECARS_FROM_32_SLOTS: LazyBlobSidecars = + LazyBlobSidecars::new(129, || { + blob_sidecars(9_481_344..=9_481_393, 7) + .into_values() + .flatten() + .collect() + }); + #[must_use] pub fn beacon_blocks( slots: RangeInclusive, From 919fa4ae293ea993199a02448f5c3d03768b1996 Mon Sep 17 00:00:00 2001 From: weekday Date: Fri, 10 Jan 2025 06:14:22 +0000 Subject: [PATCH 2/2] Optimize hexadecimal conversions in serde_utils to speed up JSON serialization of byte collections Slow serialization to JSON was claimed to be the problem that led to 2e92e00246890ae37322c9df05a3cd344e50a57e. BlobSidecar serialization is now around 15.4 times faster. BlobSidecar deserialization is now around 12 times faster. SignedBeaconBlock serialization is now up to 1.5 times faster. SignedBeaconBlock deserialization is now up to 1.1 times faster. faster-hex might be faster than const-hex in some cases, but faster-hex does not provide the API we require. See https://crates.io/crates/const-hex/1.14.0 for benchmark results comparing various hexadecimal conversion crates. --- Cargo.lock | 46 ++++++++++++++++++- Cargo.toml | 2 +- serde_utils/Cargo.toml | 2 +- .../src/prefixed_hex_or_bytes_array.rs | 2 +- serde_utils/src/prefixed_hex_or_bytes_cow.rs | 2 +- .../prefixed_hex_or_bytes_generic_array.rs | 2 +- .../src/prefixed_hex_or_bytes_slice.rs | 3 +- 7 files changed, 51 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2e668568..dc63de34 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1300,6 +1300,19 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "const-hex" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b0485bab839b018a8f1723fc5391819fea5f8f0f32288ef8a735fd096b6160c" +dependencies = [ + "cfg-if", + "cpufeatures", + "hex", + "proptest", + "serde", +] + [[package]] name = "const-oid" version = "0.9.6" @@ -6085,6 +6098,22 @@ dependencies = [ "types", ] +[[package]] +name = "proptest" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14cae93065090804185d3b75f0bf93b8eeda30c7a9b4a33d3bdb3988d6229e50" +dependencies = [ + "bitflags 2.6.0", + "lazy_static", + "num-traits", + "rand", + "rand_chacha", + "rand_xorshift", + "regex-syntax", + "unarray", +] + [[package]] name = "protobuf" version = "2.28.0" @@ -6267,6 +6296,15 @@ dependencies = [ "getrandom", ] +[[package]] +name = "rand_xorshift" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d25bf25ec5ae4a3f1b92f929810509a2f53d7dca2f50b794ff57e3face536c8f" +dependencies = [ + "rand_core", +] + [[package]] name = "rand_xoshiro" version = "0.6.0" @@ -7018,9 +7056,9 @@ name = "serde_utils" version = "0.0.0" dependencies = [ "bincode", + "const-hex", "generic-array", "hex", - "hex_fmt", "itertools 0.13.0", "num-traits", "serde", @@ -8250,6 +8288,12 @@ dependencies = [ "static_assertions", ] +[[package]] +name = "unarray" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" + [[package]] name = "unescape" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 92fd5f56..8246db26 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -300,6 +300,7 @@ bytesize = { version = '1', features = ['serde'] } cached = '0.53' chrono = '0.4' clap = { version = '4', features = ['derive'] } +const-hex = '1.14' const_format = '0.2' constant_time_eq = '0.3' conv = '0.3' @@ -339,7 +340,6 @@ hash_hasher = '2' hashlink = '0.9' hex = { version = '0.4', features = ['serde'] } hex-literal = '0.4' -hex_fmt = '0.3' hmac = '0.12' http = '1' http-body-util = '0.1' diff --git a/serde_utils/Cargo.toml b/serde_utils/Cargo.toml index 36e62599..ff22d80d 100644 --- a/serde_utils/Cargo.toml +++ b/serde_utils/Cargo.toml @@ -7,9 +7,9 @@ authors = ["Grandine "] workspace = true [dependencies] +const-hex = { workspace = true } generic-array = { workspace = true } hex = { workspace = true } -hex_fmt = { workspace = true } itertools = { workspace = true } num-traits = { workspace = true } serde = { workspace = true } diff --git a/serde_utils/src/prefixed_hex_or_bytes_array.rs b/serde_utils/src/prefixed_hex_or_bytes_array.rs index eec39b63..feb0c72a 100644 --- a/serde_utils/src/prefixed_hex_or_bytes_array.rs +++ b/serde_utils/src/prefixed_hex_or_bytes_array.rs @@ -29,7 +29,7 @@ pub fn deserialize<'de, D: Deserializer<'de>, const N: usize>( let digits = shared::strip_hex_prefix(string)?; let mut bytes = [0; N]; - hex::decode_to_slice(digits, &mut bytes).map_err(E::custom)?; + const_hex::decode_to_slice(digits, &mut bytes).map_err(E::custom)?; Ok(bytes) } diff --git a/serde_utils/src/prefixed_hex_or_bytes_cow.rs b/serde_utils/src/prefixed_hex_or_bytes_cow.rs index c2ea6e08..7e7142c8 100644 --- a/serde_utils/src/prefixed_hex_or_bytes_cow.rs +++ b/serde_utils/src/prefixed_hex_or_bytes_cow.rs @@ -34,7 +34,7 @@ pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result(self, string: &str) -> Result { let digits = shared::strip_hex_prefix(string)?; - let bytes = hex::decode(digits).map_err(E::custom)?; + let bytes = const_hex::decode(digits).map_err(E::custom)?; Ok(Cow::Owned(bytes)) } } diff --git a/serde_utils/src/prefixed_hex_or_bytes_generic_array.rs b/serde_utils/src/prefixed_hex_or_bytes_generic_array.rs index b43bbff3..9cc21f21 100644 --- a/serde_utils/src/prefixed_hex_or_bytes_generic_array.rs +++ b/serde_utils/src/prefixed_hex_or_bytes_generic_array.rs @@ -43,7 +43,7 @@ pub fn deserialize<'de, D: Deserializer<'de>, N: ArrayLength>( let digits = shared::strip_hex_prefix(string)?; let mut bytes = GenericArray::default(); - hex::decode_to_slice(digits, &mut bytes).map_err(E::custom)?; + const_hex::decode_to_slice(digits, &mut bytes).map_err(E::custom)?; Ok(bytes) } diff --git a/serde_utils/src/prefixed_hex_or_bytes_slice.rs b/serde_utils/src/prefixed_hex_or_bytes_slice.rs index 9a40f069..cc5853e2 100644 --- a/serde_utils/src/prefixed_hex_or_bytes_slice.rs +++ b/serde_utils/src/prefixed_hex_or_bytes_slice.rs @@ -1,9 +1,8 @@ -use hex_fmt::HexFmt; use serde::Serializer; pub fn serialize(bytes: impl AsRef<[u8]>, serializer: S) -> Result { if serializer.is_human_readable() { - serializer.collect_str(&format_args!("0x{}", HexFmt(bytes))) + serializer.serialize_str(const_hex::encode_prefixed(bytes).as_str()) } else { serializer.serialize_bytes(bytes.as_ref()) }