From 99937427eaea163f3602d1dba323ac4430ceba4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9ophile=20BR=C3=89ZOT?= Date: Tue, 9 Jul 2024 10:28:31 +0200 Subject: [PATCH] fixes and benches --- Cargo.toml | 1 + benches/benches.rs | 312 ++++++++++++++++++++++++++-------------- examples/insert.rs | 7 +- src/encryption_layer.rs | 21 ++- src/findex.rs | 41 +++--- src/kv.rs | 29 ++-- src/ovec.rs | 3 +- 7 files changed, 261 insertions(+), 153 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 8fa5466b..1ccca651 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,7 @@ futures = "0.3.30" cosmian_crypto_core = {git = "https://github.com/Cosmian/crypto_core.git", branch = "tbz/derive-clone-for-symkey", default-features = false, features = ["aes", "sha3"]} criterion = "0.5.1" tokio = {version = "1.38.0", features = ["rt", "macros", "rt-multi-thread", "time"]} +lazy_static = "1.5.0" [[bench]] name = "benches" diff --git a/benches/benches.rs b/benches/benches.rs index c037be80..85498a61 100644 --- a/benches/benches.rs +++ b/benches/benches.rs @@ -1,9 +1,4 @@ -#![allow(dead_code, unused)] -use std::{ - collections::{HashMap, HashSet}, - sync::{Arc, Mutex}, - time::Duration, -}; +use std::{collections::HashSet, time::Duration}; use cosmian_crypto_core::{ reexport::rand_core::{CryptoRngCore, RngCore, SeedableRng}, @@ -11,18 +6,34 @@ use cosmian_crypto_core::{ }; use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion}; use findex_bis::{dummy_decode, dummy_encode, Findex, IndexADT, KvStore, MemoryADT, Op}; -use futures::executor::block_on; +use futures::{executor::block_on, future::join_all}; +use lazy_static::lazy_static; const WORD_LENGTH: usize = 1 + 8 * 16; -/// Builds an index that associates each `kw_i` to `10^i` values, both random 64-bit values. +lazy_static! { + static ref scale: Vec = make_scale(0, 3, 8); +} + +fn make_scale(start: usize, stop: usize, n: usize) -> Vec { + let step = ((stop - start) as f32) / n as f32; + let mut points = Vec::with_capacity(n); + for i in 0..n { + points.push(start as f32 + i as f32 * step); + } + points.push(stop as f32); + points +} + +/// Builds an index that associates each `kw_i` to x values, both random 64-bit values. fn build_benchmarking_bindings_index( rng: &mut impl CryptoRngCore, ) -> Vec<([u8; 8], HashSet<[u8; 8]>)> { - (0..4) + scale + .iter() .map(|i| { let kw = rng.next_u64().to_be_bytes(); - let vals = (0..10_i64.pow(i) as usize) + let vals = (0..10f32.powf(*i).ceil() as usize) .map(|_| rng.next_u64().to_be_bytes()) .collect::>(); (kw, vals) @@ -43,60 +54,64 @@ fn build_benchmarking_keywords_index( .collect() } -fn bench_search(c: &mut Criterion) { +fn bench_search_multiple_bindings(c: &mut Criterion) { let mut rng = CsRng::from_entropy(); let seed = Secret::random(&mut rng); - // Bench the impact of the binding multiplicity. - { - let stm = KvStore::default(); - let index = build_benchmarking_bindings_index(&mut rng); - let findex = Findex::new( - seed.clone(), - Arc::new(Mutex::new(rng.clone())), - stm, - dummy_encode::, - dummy_decode, - ); - block_on(findex.insert(index.clone().into_iter())).unwrap(); + let stm = KvStore::default(); + let index = build_benchmarking_bindings_index(&mut rng); + let findex = Findex::new( + seed.clone(), + rng.clone(), + stm, + dummy_encode::, + dummy_decode, + ); + block_on(findex.insert(index.clone().into_iter())).unwrap(); - let mut group = c.benchmark_group("Multiple bindings search (1 keyword)"); - for (i, (kw, vals)) in index.clone().into_iter().enumerate() { - let n = 10i32.pow(i as u32) as usize; - group.bench_function(BenchmarkId::from_parameter(n), |b| { - b.iter_batched( - || [kw].into_iter(), - |kws| { - block_on(findex.search(kws)).expect("search failed"); - }, - criterion::BatchSize::SmallInput, - ); - }); - } + let mut group = c.benchmark_group("Multiple bindings search (1 keyword)"); + for (kw, vals) in index.clone().into_iter() { + group.bench_function(BenchmarkId::from_parameter(vals.len()), |b| { + b.iter_batched( + || { + findex.clear(); + [kw].into_iter() + }, + |kws| { + block_on(findex.search(kws)).expect("search failed"); + }, + criterion::BatchSize::SmallInput, + ); + }); } +} - // Bench the impact of the keyword multiplicity. +fn bench_search_multiple_keywords(c: &mut Criterion) { + let mut rng = CsRng::from_entropy(); + let seed = Secret::random(&mut rng); + + let stm = KvStore::default(); + let index = build_benchmarking_keywords_index(&mut rng); + let findex = Findex::new( + seed, + rng, + stm.clone(), + dummy_encode::, + dummy_decode, + ); + block_on(findex.insert(index.clone().into_iter())).unwrap(); + // Reference timings { - let stm = KvStore::default(); - let index = build_benchmarking_keywords_index(&mut rng); - let findex = Findex::new( - seed, - Arc::new(Mutex::new(rng)), - stm.clone(), - dummy_encode::, - dummy_decode, - ); - block_on(findex.insert(index.clone().into_iter())).unwrap(); - let mut group = c.benchmark_group("Multiple keywords search (1 binding)"); - for i in 0..4 { - let n = 10i32.pow(i) as usize; - group.bench_function(format!("reading {n} words from memory"), |b| { + let mut group = c.benchmark_group("retrieving words from memory"); + for i in scale.iter() { + let n = 10f32.powf(*i).ceil() as usize; + group.bench_function(BenchmarkId::from_parameter(n), |b| { // Attempts to bench all external costs (somehow, cloning the keywords impacts the // benches). b.iter_batched( || { stm.clone() .into_iter() - .map(|(a, w)| a) + .map(|(a, _)| a) .take(n) .collect::>() }, @@ -104,13 +119,20 @@ fn bench_search(c: &mut Criterion) { criterion::BatchSize::SmallInput, ); }); - // Go bench it. + } + } + // Benches + { + let mut group = c.benchmark_group("Multiple keywords search (1 binding)"); + for i in scale.iter() { + let n = 10f32.powf(*i).ceil() as usize; group.bench_function(BenchmarkId::from_parameter(n), |b| { b.iter_batched( || { + findex.clear(); // Using .cloned() instead of .clone() reduces the overhead (maybe because it // only clones what is needed) - index.iter().map(|(kw, val)| kw).take(n).cloned() + index.iter().map(|(kw, _)| kw).take(n).cloned() }, |kws| { block_on(findex.search(kws)).expect("search failed"); @@ -122,33 +144,32 @@ fn bench_search(c: &mut Criterion) { } } -fn bench_insert(c: &mut Criterion) { +fn bench_insert_multiple_bindings(c: &mut Criterion) { let mut rng = CsRng::from_entropy(); let seed = Secret::random(&mut rng); - // Bench the impact of the binding multiplicity. + let index = build_benchmarking_bindings_index(&mut rng); + let n_max = 10usize.pow(3); + + // Reference: write one word per value inserted. { - let index = build_benchmarking_bindings_index(&mut rng); - let mut group = c.benchmark_group("Multiple bindings insert (same keyword)"); - for (i, (kw, vals)) in index.clone().into_iter().enumerate() { - let n = 10i32.pow(i as u32) as usize; + let mut group = c.benchmark_group("write n words to memory"); + for (_, vals) in index.clone().into_iter() { + let stm = KvStore::with_capacity(n_max + 1); group - .bench_function(format!("inserting {n} words to memory"), |b| { + .bench_function(BenchmarkId::from_parameter(vals.len()), |b| { b.iter_batched( || { - let rng = CsRng::from_entropy(); - let seed = seed.clone(); + stm.clear(); let vals = vals.clone(); - let stm = KvStore::default(); let words = dummy_encode::(Op::Insert, vals).unwrap(); - let bindings = words + words .into_iter() .enumerate() .map(|(i, w)| ([i; 16], w)) - .collect::>(); - (stm, bindings) + .collect::>() }, - |(stm, bindings)| { + |bindings| { block_on(stm.guarded_write(([0; 16], None), bindings)) .expect("search failed"); }, @@ -156,23 +177,29 @@ fn bench_insert(c: &mut Criterion) { ); }) .measurement_time(Duration::from_secs(60)); + } + } + // Bench it + { + let mut group = c.benchmark_group("Multiple bindings insert (same keyword)"); + for (kw, vals) in index.clone().into_iter() { + let stm = KvStore::with_capacity(n_max + 1); + let findex = Findex::new( + seed.clone(), + rng.clone(), + stm.clone(), + dummy_encode::, + dummy_decode, + ); group - .bench_function(BenchmarkId::from_parameter(n), |b| { + .bench_function(BenchmarkId::from_parameter(vals.len()), |b| { b.iter_batched( || { - let seed = seed.clone(); - let vals = vals.clone(); - let findex = Findex::new( - seed, - Arc::new(Mutex::new(rng.clone())), - KvStore::default(), - dummy_encode::, - dummy_decode, - ); - let bindings = [(kw, vals)].into_iter(); - (findex, bindings) + stm.clear(); + findex.clear(); + [(kw, vals.clone())].into_iter() }, - |(findex, bindings)| { + |bindings| { block_on(findex.insert(bindings)).expect("search failed"); }, criterion::BatchSize::SmallInput, @@ -181,19 +208,24 @@ fn bench_insert(c: &mut Criterion) { .measurement_time(Duration::from_secs(60)); } } +} - // Bench the impact of the keyword multiplicity. +fn bench_insert_multiple_keywords(c: &mut Criterion) { + let mut rng = CsRng::from_entropy(); + let seed = Secret::random(&mut rng); + + // Reference: write one word per value inserted. { - let mut group = c.benchmark_group("Multiple keywords insert (one binding each)"); - for i in 0..4 { - let n = 10usize.pow(i); + let mut group = c.benchmark_group("write 2n words to memory"); + for i in scale.iter() { + let n = 10f32.powf(*i).ceil() as usize; + let stm = KvStore::with_capacity(2 * n); group - .bench_function(format!("inserting {n} words to memory"), |b| { + .bench_function(BenchmarkId::from_parameter(n), |b| { b.iter_batched( || { - let seed = seed.clone(); - let stm = KvStore::default(); - let bindings = (0..2 * n) + stm.clear(); + (0..2 * n) .map(|_| { let mut a = [0; 16]; let mut w = [0; WORD_LENGTH]; @@ -201,10 +233,9 @@ fn bench_insert(c: &mut Criterion) { rng.fill_bytes(&mut w); (a, w) }) - .collect::>(); - (stm, bindings) + .collect::>() }, - |(stm, bindings)| { + |bindings| { block_on(stm.guarded_write(([0; 16], None), bindings)) .expect("search failed"); }, @@ -212,28 +243,38 @@ fn bench_insert(c: &mut Criterion) { ); }) .measurement_time(Duration::from_secs(60)); + } + } + // Bench it + { + let mut group = c.benchmark_group("Multiple keywords insert (one binding each)"); + for i in scale.iter() { + let n = 10f32.powf(*i).ceil() as usize; + let stm = KvStore::with_capacity(2 * n); + let findex = Findex::new( + seed.clone(), + rng.clone(), + stm.clone(), + dummy_encode::, + dummy_decode, + ); group .bench_function(BenchmarkId::from_parameter(n), |b| { b.iter_batched( || { - let findex = Findex::new( - seed.clone(), - Arc::new(Mutex::new(rng.clone())), - KvStore::default(), - dummy_encode::, - dummy_decode, - ); - let bindings = (0..n) + stm.clear(); + findex.clear(); + (0..n) .map(|_| { ( rng.next_u64().to_be_bytes(), HashSet::from_iter([rng.next_u64().to_be_bytes()]), ) }) - .collect::>(); - (findex, bindings.into_iter()) + .collect::>() + .into_iter() }, - |(findex, bindings)| { + |bindings| { block_on(findex.insert(bindings)).expect("search failed"); }, criterion::BatchSize::SmallInput, @@ -244,10 +285,73 @@ fn bench_insert(c: &mut Criterion) { } } +fn bench_contention(c: &mut Criterion) { + let mut rng = CsRng::from_entropy(); + let seed = Secret::random(&mut rng); + + let mut group = c.benchmark_group("Concurrent clients (single binding, same keyword)"); + for i in scale.iter() { + let n = 10f32.powf(*i).ceil() as usize; + + let runtime = tokio::runtime::Builder::new_multi_thread() + .worker_threads(n) + .enable_all() + .build() + .unwrap(); + + let stm = KvStore::with_capacity(n + 1); + let findex = Findex::new( + seed.clone(), + rng.clone(), + stm.clone(), + dummy_encode::, + dummy_decode, + ); + + group + .bench_function(BenchmarkId::from_parameter(n), |b| { + b.iter_batched( + || { + stm.clear(); + findex.clear(); + let instances = (0..n).map(|_| findex.clone()).collect::>(); + let bindings = (0..n) + .map(|_| { + ( + rng.next_u64().to_be_bytes(), + HashSet::from_iter([rng.next_u64().to_be_bytes()]), + ) + }) + .collect::>(); + instances.into_iter().zip(bindings) + }, + |iterator| { + runtime.block_on(async { + let handles = iterator + .map(|(findex, binding)| { + tokio::spawn(async move { + findex.insert([binding].into_iter()).await + }) + }) + .collect::>(); + for res in join_all(handles).await { + res.unwrap().unwrap() + } + }) + }, + criterion::BatchSize::SmallInput, + ); + }) + .measurement_time(Duration::from_secs(60)); + } +} + criterion_group!( name = benches; config = Criterion::default().sample_size(5000); - targets = bench_search, bench_insert, + targets = bench_search_multiple_bindings, bench_search_multiple_keywords, + bench_insert_multiple_bindings, bench_insert_multiple_keywords, + bench_contention, ); criterion_main!(benches); diff --git a/examples/insert.rs b/examples/insert.rs index 21882d1e..40a3f99e 100644 --- a/examples/insert.rs +++ b/examples/insert.rs @@ -1,7 +1,4 @@ -use std::{ - collections::HashSet, - sync::{Arc, Mutex}, -}; +use std::collections::HashSet; use cosmian_crypto_core::{ reexport::rand_core::{CryptoRngCore, SeedableRng}, @@ -28,7 +25,7 @@ fn main() { let seed = Secret::random(&mut rng); let findex = Findex::new( seed, - Arc::new(Mutex::new(rng)), + rng, KvStore::default(), dummy_encode::<16, _>, dummy_decode, diff --git a/src/encryption_layer.rs b/src/encryption_layer.rs index 13edd2ef..6d1bdaaa 100644 --- a/src/encryption_layer.rs +++ b/src/encryption_layer.rs @@ -1,5 +1,5 @@ use std::{ - collections::{HashMap, HashSet}, + collections::HashMap, ops::DerefMut, sync::{Arc, Mutex, MutexGuard}, }; @@ -36,10 +36,11 @@ impl< > MemoryEncryptionLayer { /// Instantiates a new memory encryption layer. - pub fn new(seed: Secret, rng: Arc>, stm: Memory) -> Self { + pub fn new(seed: Secret, rng: CsRng, stm: Memory) -> Self { let k_p = SymmetricKey::::derive(&seed, &[0]).expect("secret is large enough"); let k_e = SymmetricKey::::derive(&seed, &[0]).expect("secret is large enough"); let aes = Aes256::new(GenericArray::from_slice(&k_p)); + let rng = Arc::new(Mutex::new(rng)); let ae = Aes256Gcm::new(&k_e); let cch = Arc::new(Mutex::new(HashMap::new())); Self { @@ -57,12 +58,8 @@ impl< } /// Retains values cached for the given keys only. - pub fn retain_cached_keys(&self, keys: &HashSet) { - self.cch - .lock() - .expect("poisoned mutex") - .deref_mut() - .retain(|k, _| keys.contains(k)); + pub fn clear(&self) { + self.cch.lock().expect("poisoned mutex").deref_mut().clear() } /// Decrypts the given value and caches the ciphertext. @@ -197,7 +194,7 @@ impl< bindings .into_iter() .zip(tokens) - .map(|(ctx, tok)| ctx.map(|ctx| self.decrypt_and_bind(ctx, &tok)).transpose()) + .map(|(ctx, tok)| ctx.map(|ctx| self.decrypt(&ctx, &tok)).transpose()) .collect() } @@ -230,8 +227,6 @@ impl< #[cfg(test)] mod tests { - use std::sync::{Arc, Mutex}; - use cosmian_crypto_core::{reexport::rand_core::SeedableRng, CsRng, Secret}; use futures::executor::block_on; @@ -249,7 +244,7 @@ mod tests { let mut rng = CsRng::from_entropy(); let seed = Secret::random(&mut rng); let kv = KvStore::, Vec>::default(); - let obf = MemoryEncryptionLayer::new(seed, Arc::new(Mutex::new(rng.clone())), kv); + let obf = MemoryEncryptionLayer::new(seed, rng.clone(), kv); let tok = Address::::random(&mut rng); let ptx = [1; WORD_LENGTH]; let ctx = obf.encrypt(&ptx, &tok).unwrap(); @@ -266,7 +261,7 @@ mod tests { let mut rng = CsRng::from_entropy(); let seed = Secret::random(&mut rng); let kv = KvStore::, Vec>::default(); - let obf = MemoryEncryptionLayer::new(seed, Arc::new(Mutex::new(rng.clone())), kv); + let obf = MemoryEncryptionLayer::new(seed, rng.clone(), kv); let header_addr = Address::::random(&mut rng); diff --git a/src/findex.rs b/src/findex.rs index a4402a7a..24c951bd 100644 --- a/src/findex.rs +++ b/src/findex.rs @@ -11,6 +11,7 @@ use crate::{ ovec::IVec, Address, IndexADT, MemoryADT, ADDRESS_LENGTH, KEY_LENGTH, }; +#[derive(Clone, Debug)] pub struct Findex< const WORD_LENGTH: usize, Value, @@ -22,20 +23,22 @@ pub struct Findex< Memory::Error: Send + Sync, { el: MemoryEncryptionLayer, - vectors: Mutex< - HashMap< - Address, - IVec>, + vectors: Arc< + Mutex< + HashMap< + Address, + IVec>, + >, >, >, - encode: Box< + encode: Arc< fn( Op, HashSet, ) -> Result as MemoryADT>::Word>, String>, >, - decode: Box< + decode: Arc< fn( Vec< as MemoryADT>::Word>, ) -> Result, TryFromError>, @@ -56,7 +59,7 @@ where /// Instantiates Findex with the given seed, and memory. pub fn new( seed: Secret, - rng: Arc>, + rng: CsRng, mem: Memory, encode: fn(Op, HashSet) -> Result, String>, decode: fn(Vec<[u8; WORD_LENGTH]>) -> Result, TryFromError>, @@ -66,12 +69,17 @@ where // waiting for the lock => bench it. Self { el: MemoryEncryptionLayer::new(seed, rng, mem), - vectors: Mutex::new(HashMap::new()), - encode: Box::new(encode), - decode: Box::new(decode), + vectors: Arc::new(Mutex::new(HashMap::new())), + encode: Arc::new(encode), + decode: Arc::new(decode), } } + pub fn clear(&self) { + self.vectors.lock().unwrap().clear(); + self.el.clear(); + } + /// Caches this vector for this address. fn bind( &self, @@ -211,10 +219,7 @@ where #[cfg(test)] mod tests { - use std::{ - collections::{HashMap, HashSet}, - sync::{Arc, Mutex}, - }; + use std::collections::{HashMap, HashSet}; use cosmian_crypto_core::{reexport::rand_core::SeedableRng, CsRng, Secret}; use futures::executor::block_on; @@ -233,13 +238,7 @@ mod tests { let mut rng = CsRng::from_entropy(); let seed = Secret::random(&mut rng); let kv = KvStore::, Vec>::default(); - let findex = Findex::new( - seed, - Arc::new(Mutex::new(rng)), - kv, - dummy_encode::, - dummy_decode, - ); + let findex = Findex::new(seed, rng, kv, dummy_encode::, dummy_decode); let bindings = HashMap::<&str, HashSet>::from_iter([ ( "cat", diff --git a/src/kv.rs b/src/kv.rs index f37132a0..4faf0067 100644 --- a/src/kv.rs +++ b/src/kv.rs @@ -19,18 +19,27 @@ impl Display for MemoryError { impl std::error::Error for MemoryError {} #[derive(Clone, Debug)] -pub struct KvStore(Arc>>); +pub struct KvStore { + inner: Arc>>, +} impl Default for KvStore { fn default() -> Self { - Self(Arc::new(Mutex::new(HashMap::new()))) + Self { + inner: Arc::new(Mutex::new(HashMap::new())), + } } } impl KvStore { + pub fn with_capacity(c: usize) -> Self { + Self { + inner: Arc::new(Mutex::new(HashMap::with_capacity(c))), + } + } + pub fn clear(&self) { - let store = &mut *self.0.lock().expect("poisoned lock"); - store.clear() + self.inner.lock().expect("poisoned lock").clear(); } } @@ -44,8 +53,8 @@ impl) -> Result>, Self::Error> { - let store = &mut *self.0.lock().expect("poisoned lock"); - Ok(a.into_iter().map(|k| store.get(&k).cloned()).collect()) + let store = self.inner.lock().expect("poisoned lock"); + Ok(a.iter().map(|k| store.get(k).cloned()).collect()) } async fn guarded_write( @@ -53,7 +62,7 @@ impl), bindings: Vec<(Self::Address, Self::Word)>, ) -> Result, Self::Error> { - let store = &mut *self.0.lock().expect("poisoned lock"); + let store = &mut *self.inner.lock().expect("poisoned lock"); let (a, old) = guard; let cur = store.get(&a).cloned(); if old == cur { @@ -74,7 +83,11 @@ impl IntoIterator type IntoIter = as IntoIterator>::IntoIter; fn into_iter(self) -> Self::IntoIter { - self.0.lock().expect("poisoned lock").clone().into_iter() + self.inner + .lock() + .expect("poisoned lock") + .clone() + .into_iter() } } diff --git a/src/ovec.rs b/src/ovec.rs index bad18e37..ea2a275b 100644 --- a/src/ovec.rs +++ b/src/ovec.rs @@ -219,7 +219,6 @@ mod tests { ADDRESS_LENGTH, }; use cosmian_crypto_core::{reexport::rand_core::SeedableRng, CsRng, Secret}; - use std::sync::{Arc, Mutex}; const WORD_LENGTH: usize = 16; @@ -228,7 +227,7 @@ mod tests { let mut rng = CsRng::from_entropy(); let seed = Secret::random(&mut rng); let kv = KvStore::, Vec>::default(); - let obf = MemoryEncryptionLayer::new(seed, Arc::new(Mutex::new(rng.clone())), kv.clone()); + let obf = MemoryEncryptionLayer::new(seed, rng.clone(), kv.clone()); let address = Address::random(&mut rng); let v = IVec::::new(address.clone(), obf); test_vector_sequential(&v).await;