Skip to content

Commit 83cbdba

Browse files
feat(suon_xtea): make expand const fn, rewrite benchmarks with hot+cold cache, throughput, improved naming
1 parent ec83a95 commit 83cbdba

2 files changed

Lines changed: 199 additions & 65 deletions

File tree

‎crates/suon_xtea/benches/xtea.rs‎

Lines changed: 187 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,86 +1,209 @@
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};
28
use std::hint::black_box;
3-
use suon_xtea::{Key, decrypt, encrypt, expand};
9+
use suon_xtea::{ExpandedKey, Key, decrypt, encrypt, expand};
410

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];
613

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;
1243

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)));
1854
});
1955
}
2056

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();
2871
}
2972

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();
36100
}
37101

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();
45129
}
46130

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();
53163
}
54164

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+
});
61176
});
177+
group.finish();
62178
}
63179

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+
}
73196
}
74197

75198
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,
85208
);
86-
criterion_main!(benches);
209+
criterion_main!(xtea_benchmarks);

‎crates/suon_xtea/src/lib.rs‎

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,8 +92,11 @@ fn mix(v: u32) -> u32 {
9292
/// The output layout is interleaved:
9393
/// `expanded[2*i]` = left round key for round i
9494
/// `expanded[2*i+1]` = right round key for round i
95+
///
96+
/// This function is `const`, so the expanded key can be computed at compile time
97+
/// when the 128-bit key is known upfront.
9598
#[inline(always)]
96-
pub fn expand(key: &Key) -> ExpandedKey {
99+
pub const fn expand(key: &Key) -> ExpandedKey {
97100
let mut round_keys = [0u32; ROUNDS * 2];
98101

99102
// Running sum that advances by DELTA each round.
@@ -425,6 +428,14 @@ mod tests {
425428
);
426429
}
427430

431+
#[test]
432+
fn expand_const_fn() {
433+
// Asserts that `expand` can be evaluated at compile time.
434+
const EXPANDED: ExpandedKey = expand(&[0; 4]);
435+
let expanded = expand(&[0; 4]);
436+
assert_eq!(EXPANDED, expanded);
437+
}
438+
428439
#[test]
429440
fn expand_key_one_shot_convenience() {
430441
let key = [0x0123_4567, 0x89AB_CDEF, 0xFEDC_BA98, 0x7654_3210];

0 commit comments

Comments
 (0)