|
1 | | -use criterion::{Criterion, criterion_group, criterion_main}; |
| 1 | +//! Benchmarks for the suon_xtea crate. |
| 2 | +//! |
| 3 | +//! Measures encryption and decryption throughput across realistic packet |
| 4 | +//! sizes under both hot cache and cold cache scenarios. Packet sizes |
| 5 | +//! reflect typical protocol payloads. |
| 6 | +
|
| 7 | +use criterion::{Criterion, Throughput, criterion_group, criterion_main}; |
2 | 8 | use std::hint::black_box; |
3 | | -use suon_xtea::{Key, decrypt, encrypt, expand}; |
| 9 | +use suon_xtea::{ExpandedKey, Key, decrypt, encrypt, expand}; |
4 | 10 |
|
5 | | -const KEY_128: Key = [0x0123_4567, 0x89AB_CDEF, 0xFEDC_BA98, 0x7654_3210]; |
| 11 | +/// 128-bit XTEA key shared by all benchmarks. |
| 12 | +const XTEA_KEY_128_BIT: Key = [0x0123_4567, 0x89AB_CDEF, 0xFEDC_BA98, 0x7654_3210]; |
6 | 13 |
|
7 | | -fn xtea_expand_key(c: &mut Criterion) { |
8 | | - c.bench_function("xtea_expand_key", |b| { |
9 | | - b.iter(|| expand(black_box(&KEY_128))) |
10 | | - }); |
11 | | -} |
| 14 | +/// Byte value used to fill every benchmark buffer. |
| 15 | +const PLAINTEXT_PATTERN: u8 = 0xAB; |
| 16 | + |
| 17 | +/// Expanded round keys for [`XTEA_KEY_128_BIT`]. |
| 18 | +/// |
| 19 | +/// Evaluated at compile time so no runtime setup cost is paid by any |
| 20 | +/// individual benchmark. |
| 21 | +const EXPANDED_KEYS: ExpandedKey = expand(&XTEA_KEY_128_BIT); |
| 22 | + |
| 23 | +/// Memory pool size for cold cache benchmarks. |
| 24 | +/// |
| 25 | +/// Large enough to exceed the CPU cache (64 mebibytes) so the rotating |
| 26 | +/// stride evicts previously accessed regions. |
| 27 | +/// |
| 28 | +/// A fresh [`pool.fill()`](Vec::fill) call between sizes guarantees that |
| 29 | +/// each benchmark starts from the same clean state, regardless of |
| 30 | +/// mutations from the previous size. |
| 31 | +const COLD_CACHE_POOL_SIZE: usize = 64 * 1024 * 1024; |
| 32 | + |
| 33 | +/// Packet sizes benchmarked under the cold cache scenario. |
| 34 | +/// |
| 35 | +/// Limited to the most common realistic sizes because allocating and |
| 36 | +/// striding through a 64-mebibyte pool is expensive. |
| 37 | +const COLD_CACHE_SIZES: &[usize] = &[64, 256, 1024]; |
| 38 | + |
| 39 | +/// Size used for the roundtrip benchmark (encrypt + decrypt). |
| 40 | +/// |
| 41 | +/// 1024 bytes is the most realistic packet size for this scenario. |
| 42 | +const ROUNDTRIP_SIZE: usize = 1024; |
12 | 43 |
|
13 | | -fn xtea_encrypt_single_block(c: &mut Criterion) { |
14 | | - let round_keys = expand(&KEY_128); |
15 | | - let mut buffer = vec![0xABu8; 8]; |
16 | | - c.bench_function("xtea_encrypt_8_bytes", |b| { |
17 | | - b.iter(|| encrypt(black_box(&mut buffer), &round_keys).unwrap()) |
| 44 | +/// Packet sizes that represent realistic protocol payloads. |
| 45 | +/// |
| 46 | +/// Includes a single-block minimum (8 bytes), an odd-block count |
| 47 | +/// (24 bytes = 3 blocks), common small-to-large packets, and a |
| 48 | +/// throughput measurement size (1 mebibyte). |
| 49 | +const PACKET_SIZES: &[usize] = &[8, 24, 64, 128, 256, 512, 1024, 1024 * 1024]; |
| 50 | + |
| 51 | +fn expand_key(criterion: &mut Criterion) { |
| 52 | + criterion.bench_function("xtea/expand_key", |bencher| { |
| 53 | + bencher.iter(|| expand(black_box(&XTEA_KEY_128_BIT))); |
18 | 54 | }); |
19 | 55 | } |
20 | 56 |
|
21 | | -fn xtea_decrypt_single_block(c: &mut Criterion) { |
22 | | - let round_keys = expand(&KEY_128); |
23 | | - let mut buffer = vec![0xABu8; 8]; |
24 | | - encrypt(&mut buffer, &round_keys).unwrap(); |
25 | | - c.bench_function("xtea_decrypt_8_bytes", |b| { |
26 | | - b.iter(|| decrypt(black_box(&mut buffer), &round_keys).unwrap()) |
27 | | - }); |
| 57 | +fn encrypt_hot_cache(criterion: &mut Criterion) { |
| 58 | + let mut group = criterion.benchmark_group("xtea/encrypt"); |
| 59 | + for &packet_size in PACKET_SIZES { |
| 60 | + group.throughput(Throughput::Bytes(packet_size as u64)); |
| 61 | + group.bench_function(format_size(packet_size), |bencher| { |
| 62 | + let mut buffer = vec![PLAINTEXT_PATTERN; packet_size]; |
| 63 | + bencher.iter(|| { |
| 64 | + encrypt(black_box(&mut buffer), &EXPANDED_KEYS) |
| 65 | + .expect("encryption of fixed-size buffer must succeed"); |
| 66 | + }); |
| 67 | + }); |
| 68 | + } |
| 69 | + |
| 70 | + group.finish(); |
28 | 71 | } |
29 | 72 |
|
30 | | -fn xtea_encrypt_1_kibibyte(c: &mut Criterion) { |
31 | | - let round_keys = expand(&KEY_128); |
32 | | - let mut buffer = vec![0xABu8; 1024]; |
33 | | - c.bench_function("xtea_encrypt_1024_bytes", |b| { |
34 | | - b.iter(|| encrypt(black_box(&mut buffer), &round_keys).unwrap()) |
35 | | - }); |
| 73 | +fn decrypt_hot_cache(criterion: &mut Criterion) { |
| 74 | + // Pre-encrypts one buffer per size so the decryption benchmark has |
| 75 | + // valid ciphertext to operate on. |
| 76 | + let mut buffers: Vec<Vec<u8>> = PACKET_SIZES |
| 77 | + .iter() |
| 78 | + .map(|&packet_size| { |
| 79 | + let mut buffer = vec![PLAINTEXT_PATTERN; packet_size]; |
| 80 | + encrypt(&mut buffer, &EXPANDED_KEYS) |
| 81 | + .expect("buffer encryption for decryption benchmark setup must succeed"); |
| 82 | + buffer |
| 83 | + }) |
| 84 | + .collect(); |
| 85 | + |
| 86 | + let mut group = criterion.benchmark_group("xtea/decrypt"); |
| 87 | + |
| 88 | + for (index, &packet_size) in PACKET_SIZES.iter().enumerate() { |
| 89 | + group.throughput(Throughput::Bytes(packet_size as u64)); |
| 90 | + group.bench_function(format_size(packet_size), |bencher| { |
| 91 | + let buffer = &mut buffers[index]; |
| 92 | + bencher.iter(|| { |
| 93 | + decrypt(black_box(buffer.as_mut_slice()), &EXPANDED_KEYS) |
| 94 | + .expect("decryption of fixed-size buffer must succeed"); |
| 95 | + }); |
| 96 | + }); |
| 97 | + } |
| 98 | + |
| 99 | + group.finish(); |
36 | 100 | } |
37 | 101 |
|
38 | | -fn xtea_decrypt_1_kibibyte(c: &mut Criterion) { |
39 | | - let round_keys = expand(&KEY_128); |
40 | | - let mut buffer = vec![0xABu8; 1024]; |
41 | | - encrypt(&mut buffer, &round_keys).unwrap(); |
42 | | - c.bench_function("xtea_decrypt_1024_bytes", |b| { |
43 | | - b.iter(|| decrypt(black_box(&mut buffer), &round_keys).unwrap()) |
44 | | - }); |
| 102 | +fn encrypt_cold_cache(criterion: &mut Criterion) { |
| 103 | + let mut pool: Vec<u8> = vec![PLAINTEXT_PATTERN; COLD_CACHE_POOL_SIZE]; |
| 104 | + |
| 105 | + let mut group = criterion.benchmark_group("xtea/encrypt/cold_cache"); |
| 106 | + |
| 107 | + for &packet_size in COLD_CACHE_SIZES { |
| 108 | + // Resets the pool to a clean state so every size starts with |
| 109 | + // identical data, regardless of mutations from previous runs. |
| 110 | + pool.fill(PLAINTEXT_PATTERN); |
| 111 | + |
| 112 | + group.throughput(Throughput::Bytes(packet_size as u64)); |
| 113 | + group.bench_function(format_size(packet_size), |bencher| { |
| 114 | + let mut offset: usize = 0; |
| 115 | + bencher.iter(|| { |
| 116 | + let start: usize = offset; |
| 117 | + offset = (start + packet_size) % (COLD_CACHE_POOL_SIZE - packet_size); |
| 118 | + |
| 119 | + encrypt( |
| 120 | + black_box(&mut pool[start..start + packet_size]), |
| 121 | + &EXPANDED_KEYS, |
| 122 | + ) |
| 123 | + .expect("encryption of cold cache pool slice must succeed"); |
| 124 | + }); |
| 125 | + }); |
| 126 | + } |
| 127 | + |
| 128 | + group.finish(); |
45 | 129 | } |
46 | 130 |
|
47 | | -fn xtea_encrypt_1_mebibyte(c: &mut Criterion) { |
48 | | - let round_keys = expand(&KEY_128); |
49 | | - let mut buffer = vec![0xABu8; 1024 * 1024]; |
50 | | - c.bench_function("xtea_encrypt_1_mebibyte", |b| { |
51 | | - b.iter(|| encrypt(black_box(&mut buffer), &round_keys).unwrap()) |
52 | | - }); |
| 131 | +fn decrypt_cold_cache(criterion: &mut Criterion) { |
| 132 | + let mut pool: Vec<u8> = vec![PLAINTEXT_PATTERN; COLD_CACHE_POOL_SIZE]; |
| 133 | + |
| 134 | + let mut group = criterion.benchmark_group("xtea/decrypt/cold_cache"); |
| 135 | + |
| 136 | + for &packet_size in COLD_CACHE_SIZES { |
| 137 | + // Resets the pool to a clean state so every size starts with |
| 138 | + // identical data, regardless of mutations from previous runs. |
| 139 | + pool.fill(PLAINTEXT_PATTERN); |
| 140 | + |
| 141 | + group.throughput(Throughput::Bytes(packet_size as u64)); |
| 142 | + group.bench_function(format_size(packet_size), |bencher| { |
| 143 | + // XTEA uses only arithmetic operations (wrapping_add, |
| 144 | + // wrapping_sub, xor, shifts) with no data-dependent branches |
| 145 | + // or table lookups. The computational cost of decrypting |
| 146 | + // plaintext is therefore identical to decrypting valid |
| 147 | + // ciphertext, which lets us skip a pre-encryption pass that |
| 148 | + // would warm the pool and defeat the cold cache measurement. |
| 149 | + let mut offset: usize = 0; |
| 150 | + bencher.iter(|| { |
| 151 | + let start: usize = offset; |
| 152 | + offset = (start + packet_size) % (COLD_CACHE_POOL_SIZE - packet_size); |
| 153 | + decrypt( |
| 154 | + black_box(&mut pool[start..start + packet_size]), |
| 155 | + &EXPANDED_KEYS, |
| 156 | + ) |
| 157 | + .expect("decryption of cold cache pool slice must succeed"); |
| 158 | + }); |
| 159 | + }); |
| 160 | + } |
| 161 | + |
| 162 | + group.finish(); |
53 | 163 | } |
54 | 164 |
|
55 | | -fn xtea_decrypt_1_mebibyte(c: &mut Criterion) { |
56 | | - let round_keys = expand(&KEY_128); |
57 | | - let mut buffer = vec![0xABu8; 1024 * 1024]; |
58 | | - encrypt(&mut buffer, &round_keys).unwrap(); |
59 | | - c.bench_function("xtea_decrypt_1_mebibyte", |b| { |
60 | | - b.iter(|| decrypt(black_box(&mut buffer), &round_keys).unwrap()) |
| 165 | +fn roundtrip(criterion: &mut Criterion) { |
| 166 | + let mut group = criterion.benchmark_group("xtea/roundtrip"); |
| 167 | + group.throughput(Throughput::Bytes(ROUNDTRIP_SIZE as u64)); |
| 168 | + group.bench_function(format_size(ROUNDTRIP_SIZE), |bencher| { |
| 169 | + let mut buffer = vec![PLAINTEXT_PATTERN; ROUNDTRIP_SIZE]; |
| 170 | + bencher.iter(|| { |
| 171 | + encrypt(black_box(&mut buffer), &EXPANDED_KEYS) |
| 172 | + .expect("encryption in roundtrip benchmark must succeed"); |
| 173 | + decrypt(black_box(&mut buffer), &EXPANDED_KEYS) |
| 174 | + .expect("decryption in roundtrip benchmark must succeed"); |
| 175 | + }); |
61 | 176 | }); |
| 177 | + group.finish(); |
62 | 178 | } |
63 | 179 |
|
64 | | -fn xtea_roundtrip_1_mebibyte(c: &mut Criterion) { |
65 | | - let round_keys = expand(&KEY_128); |
66 | | - let mut buffer = vec![0xABu8; 1024 * 1024]; |
67 | | - c.bench_function("xtea_roundtrip_1_mebibyte", |b| { |
68 | | - b.iter(|| { |
69 | | - encrypt(black_box(&mut buffer), &round_keys).unwrap(); |
70 | | - decrypt(black_box(&mut buffer), &round_keys).unwrap(); |
71 | | - }) |
72 | | - }); |
| 180 | +/// Formats a byte count into a human-readable benchmark label. |
| 181 | +/// |
| 182 | +/// Sizes below one mebibyte use a plain number suffixed with `_bytes`. |
| 183 | +/// Sizes of one mebibyte or larger use a mebibyte count so the label |
| 184 | +/// stays readable. |
| 185 | +fn format_size(bytes: usize) -> String { |
| 186 | + if bytes >= 1024 * 1024 { |
| 187 | + let mebibytes = bytes / (1024 * 1024); |
| 188 | + if mebibytes == 1 { |
| 189 | + "1_mebibyte".to_string() |
| 190 | + } else { |
| 191 | + format!("{mebibytes}_mebibytes") |
| 192 | + } |
| 193 | + } else { |
| 194 | + format!("{}_bytes", bytes) |
| 195 | + } |
73 | 196 | } |
74 | 197 |
|
75 | 198 | criterion_group!( |
76 | | - benches, |
77 | | - xtea_expand_key, |
78 | | - xtea_encrypt_single_block, |
79 | | - xtea_decrypt_single_block, |
80 | | - xtea_encrypt_1_kibibyte, |
81 | | - xtea_decrypt_1_kibibyte, |
82 | | - xtea_encrypt_1_mebibyte, |
83 | | - xtea_decrypt_1_mebibyte, |
84 | | - xtea_roundtrip_1_mebibyte, |
| 199 | + name = xtea_benchmarks; |
| 200 | + config = Criterion::default(); |
| 201 | + targets = |
| 202 | + expand_key, |
| 203 | + encrypt_hot_cache, |
| 204 | + decrypt_hot_cache, |
| 205 | + encrypt_cold_cache, |
| 206 | + decrypt_cold_cache, |
| 207 | + roundtrip, |
85 | 208 | ); |
86 | | -criterion_main!(benches); |
| 209 | +criterion_main!(xtea_benchmarks); |
0 commit comments