diff --git a/Cargo.lock b/Cargo.lock index 21e35435fe4e87..bd180b1a464446 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5413,6 +5413,7 @@ dependencies = [ "rustc_version 0.4.0", "seqlock", "serde", + "serde_bytes", "serde_derive", "smallvec", "solana-accounts-db", diff --git a/accounts-db/Cargo.toml b/accounts-db/Cargo.toml index 1a3abb6c04ea88..8a8eb1d8cd4324 100644 --- a/accounts-db/Cargo.toml +++ b/accounts-db/Cargo.toml @@ -63,6 +63,7 @@ ed25519-dalek = { workspace = true } libsecp256k1 = { workspace = true } memoffset = { workspace = true } rand_chacha = { workspace = true } +serde_bytes = { workspace = true } # See order-crates-for-publishing.py for using this unusual `path = "."` solana-accounts-db = { path = ".", features = ["dev-context-only-utils"] } solana-logger = { workspace = true } @@ -88,3 +89,7 @@ harness = false [[bench]] name = "bench_hashing" harness = false + +[[bench]] +name = "bench_serde" +harness = false diff --git a/accounts-db/benches/bench_serde.rs b/accounts-db/benches/bench_serde.rs new file mode 100644 index 00000000000000..73cb7aadc98302 --- /dev/null +++ b/accounts-db/benches/bench_serde.rs @@ -0,0 +1,131 @@ +use { + criterion::{criterion_group, criterion_main, BatchSize, BenchmarkId, Criterion, Throughput}, + serde::{Deserialize, Serialize}, + solana_sdk::{account::Account, clock::Epoch, pubkey::Pubkey}, + std::mem, +}; + +const KB: usize = 1024; +const MB: usize = KB * KB; + +const DATA_SIZES: [usize; 4] = [ + 0, // the smallest account + 200, // the size of a stake account + MB, // the max size of bincode's internal buffer + 10 * MB, // the largest account +]; + +/// Benchmark how long it takes to serialize an account +fn bench_account_serialize(c: &mut Criterion) { + let mut group = c.benchmark_group("serde_account_serialize"); + for data_size in DATA_SIZES { + let account = Account::new(0, data_size, &Pubkey::default()); + let num_bytes = bincode::serialized_size(&account).unwrap(); + group.throughput(Throughput::Bytes(num_bytes)); + + // serialize the account, treating the data as a sequence + let account_with_data_as_seq = AccountWithDataAsSeq { + data: vec![0; data_size], + ..Default::default() + }; + group.bench_function(BenchmarkId::new("data_as_seq", data_size), |b| { + b.iter_batched( + || Vec::with_capacity(num_bytes as usize), + |buffer| bincode::serialize_into(buffer, &account_with_data_as_seq).unwrap(), + BatchSize::PerIteration, + ); + }); + + // serialize the account, treating the data as bytes + let account_with_data_as_bytes = AccountWithDataAsBytes { + data: vec![0; data_size], + ..Default::default() + }; + group.bench_function(BenchmarkId::new("data_as_bytes", data_size), |b| { + b.iter_batched( + || Vec::with_capacity(num_bytes as usize), + |buffer| bincode::serialize_into(buffer, &account_with_data_as_bytes).unwrap(), + BatchSize::PerIteration, + ); + }); + } +} + +/// Benchmark how long it takes to deserialize an account +fn bench_account_deserialize(c: &mut Criterion) { + let mut group = c.benchmark_group("serde_account_deserialize"); + for data_size in DATA_SIZES { + let account = Account::new(0, data_size, &Pubkey::default()); + let serialized_account = bincode::serialize(&account).unwrap(); + let num_bytes = serialized_account.len() as u64; + group.throughput(Throughput::Bytes(num_bytes)); + + // deserialize the account, treating the data as a sequence + let serialized_account = bincode::serialize(&AccountWithDataAsSeq { + data: vec![0; data_size], + ..Default::default() + }) + .unwrap(); + group.bench_function(BenchmarkId::new("data_as_seq", data_size), |b| { + b.iter_batched( + || (), + |()| { + bincode::deserialize::(serialized_account.as_slice()) + .unwrap() + }, + BatchSize::PerIteration, + ); + }); + + // deserialize the account, treating the data as bytes + let serialized_account = bincode::serialize(&AccountWithDataAsBytes { + data: vec![0; data_size], + ..Default::default() + }) + .unwrap(); + group.bench_function(BenchmarkId::new("data_as_bytes", data_size), |b| { + b.iter_batched( + || (), + |()| { + bincode::deserialize::(serialized_account.as_slice()) + .unwrap() + }, + BatchSize::PerIteration, + ); + }); + } +} + +/// An account, with normal serde behavior. +/// The account data will be serialized as a sequence. +#[repr(C)] +#[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Default)] +struct AccountWithDataAsSeq { + lamports: u64, + data: Vec, + owner: Pubkey, + executable: bool, + rent_epoch: Epoch, +} + +// Ensure that our new account type stays in-sync with the real Account type. +const _: () = assert!(mem::size_of::() == mem::size_of::()); + +/// An account, with modified serde behavior. +/// The account data will be serialized as bytes. +#[repr(C)] +#[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Default)] +struct AccountWithDataAsBytes { + lamports: u64, + #[serde(with = "serde_bytes")] + data: Vec, + owner: Pubkey, + executable: bool, + rent_epoch: Epoch, +} + +// Ensure that our new account type stays in-sync with the real Account type. +const _: () = assert!(mem::size_of::() == mem::size_of::()); + +criterion_group!(benches, bench_account_serialize, bench_account_deserialize); +criterion_main!(benches);