diff --git a/README.md b/README.md index 4190c9a..88ab460 100644 --- a/README.md +++ b/README.md @@ -359,13 +359,14 @@ The Criterion suite in `benches/hot_path.rs` needs the `encryption` feature (`ma | Case | What one call does | |:-----|:-------------------| | `store/`, `retrieve/` | `ByteStorage::store` / `retrieve` on the realistic msgpack payload `hot_path` uses | +| `retrieve_data/` | `ByteStorage::retrieve_data`, the bindings' call, on the same envelope as `retrieve` | | `prescan/` | `check_msgpack_structure` on the envelope, the pre-scan `retrieve` runs first | | `encrypt/`, `decrypt/` | `ZeroKnowledgeEncryptor` AES-256-GCM | | `keyring_decrypt/` | `Keyring::decrypt_indexed`, which re-derives the tenant key (HKDF) on every call | | `tenant_keyring_decrypt/` | `TenantKeyring::decrypt_indexed`, key derived once at `for_tenant` | | `hkdf` | `derive_domain_key` (takes no payload, so it has no size) | -Sizes are 64 B, 1 KiB and 64 KiB. `store` and `retrieve` also run at 40 B, below the 64-byte literals-only block threshold, and at 1 MiB. `ByteStorage` switches to its copying allocation pattern once the envelope or the compressed payload reaches 64 KiB. The 64 KiB payload compresses to a 24,702-byte envelope and never gets there; the 1 MiB one compresses to 383,893 bytes. One case at a time: `make perf-ir ARGS="--case store/1024"`. +Sizes are 64 B, 1 KiB and 64 KiB. `store`, `retrieve` and `retrieve_data` also run at 40 B, below the 64-byte literals-only block threshold, and at 1 MiB. `ByteStorage` switches to its copying allocation pattern once the envelope or the compressed payload reaches 64 KiB. The 64 KiB payload compresses to a 24,702-byte envelope and never gets there; the 1 MiB one compresses to 383,893 bytes. One case at a time: `make perf-ir ARGS="--case store/1024"`. The bench binary is built with fat LTO, so its cases shape each other's code. Because both `retrieve` and `retrieve_data` call the envelope decode, it stays out of line, and `retrieve` costs 68 to 77 Ir/op more at 40 B to 1 KiB (2.6% at 40 B) than in a binary that calls `retrieve` alone. A consumer that calls only one of the two gets the decode inlined. **Method** (`benches/perf_ir.rs`): each case runs in its own process under `valgrind --tool=cachegrind --cache-sim=no`, once with N calls and once with none, after the same setup and one warm-up call. N is 1,000, or 20 for a 1 MiB case. Ir/op is `(Ir[N] - Ir[0]) / N`, so process start, setup and exit cancel. The counts are exact, so N sets the run time, not the precision. The measured process gets an empty environment, so neither the shell nor a `VALGRIND_OPTS` changes what is counted. diff --git a/benches/perf_ir.rs b/benches/perf_ir.rs index b713041..101a78e 100644 --- a/benches/perf_ir.rs +++ b/benches/perf_ir.rs @@ -75,7 +75,8 @@ const BASELINES: &str = concat!( const MIB: usize = 1024 * 1024; /// A small value, a typical value, a large object (as `hot_path`). const SIZES: &[usize] = &[64, 1024, 64 * 1024]; -/// ByteStorage picks its strategy by size, so store and retrieve also measure +/// ByteStorage picks its strategy by size, so store and retrieve (and +/// retrieve_data, which decodes the same envelope) also measure /// 40 B, under the literals-only block threshold (64 B), and 1 MiB. Its /// large-payload switch compares the envelope (store) or the compressed /// payload (retrieve) with 64 KiB: the 64 KiB payload stores as a @@ -85,9 +86,10 @@ const STORAGE_SIZES: &[usize] = &[40, 64, 1024, 64 * 1024, MIB]; /// Operations and their payload sizes. HKDF derives from the master key and /// tenant id alone, so it has one case with no size. -const OPS: [(&str, &[usize]); 8] = [ +const OPS: [(&str, &[usize]); 9] = [ ("store", STORAGE_SIZES), ("retrieve", STORAGE_SIZES), + ("retrieve_data", STORAGE_SIZES), ("prescan", SIZES), ("encrypt", SIZES), ("decrypt", SIZES), @@ -152,6 +154,12 @@ fn operation(case: &str) -> Box usize> { match op { "store" => Box::new(move || storage.store(black_box(&payload), None).unwrap().len()), "retrieve" => Box::new(move || storage.retrieve(black_box(&envelope)).unwrap().0.len()), + // What the bindings call: retrieve without the format String. Under + // fat LTO this second caller keeps decode_envelope out of line, which + // costs retrieve 68 to 77 Ir/op at 40 B to 1 KiB (README). + "retrieve_data" => { + Box::new(move || storage.retrieve_data(black_box(&envelope)).unwrap().len()) + } "prescan" => Box::new(move || { check_msgpack_structure(black_box(&envelope), ENVELOPE_MAX_DEPTH).is_ok() as usize }), diff --git a/benches/perf_ir_baselines.json b/benches/perf_ir_baselines.json index ef18ef0..d58557f 100644 --- a/benches/perf_ir_baselines.json +++ b/benches/perf_ir_baselines.json @@ -18,11 +18,16 @@ "prescan/1024": 985, "prescan/64": 883, "prescan/65536": 935, - "retrieve/1024": 7323, - "retrieve/1048576": 6676538, - "retrieve/40": 2560, - "retrieve/64": 2552, - "retrieve/65536": 391697, + "retrieve/1024": 7409, + "retrieve/1048576": 6679353, + "retrieve/40": 2637, + "retrieve/64": 2630, + "retrieve/65536": 391975, + "retrieve_data/1024": 7243, + "retrieve_data/1048576": 6679184, + "retrieve_data/40": 2471, + "retrieve_data/64": 2464, + "retrieve_data/65536": 391809, "store/1024": 22934, "store/1048576": 11912874, "store/40": 1186, @@ -39,6 +44,8 @@ "prescan/65536": 0, "retrieve/1048576": 0, "retrieve/65536": 0, + "retrieve_data/1048576": 0, + "retrieve_data/65536": 0, "store/1048576": 0, "store/65536": 0, "tenant_keyring_decrypt/65536": 0