Skip to content

Commit f4a45d9

Browse files
committed
Add optional CPU cycle measurement to graviola-bench
* This was motivated by the observation that the elapsed time for the Graviola benchmarks on macOS can sometimes vary by 10% from one run to the next, probably due to some combination of varying CPU frequency and random background daemons. In past projects, I've found that the CPU's cycle counter, while still noisy, is less noisy than the wall clock. * This change adds a feature `__bench_cycles` that replaces the standard Criterion elapsed time metric with a measurement of elapsed CPU cycles for the process on aarch64. * It should be straightforward to add x86_64 support in a later change, either with inline assembly or by using the Linux `perf` API. (The latter could also provide access to metrics like instructions retired and branch mispredictions.) * Because the counter-timer register is per-core, this implementation may return incorrect results if a single benchmark runs long enough for the OS to reschedule it to a different core or to interrupt the execution with some other process. It may be beneficial to add CPU pinning in the future.
1 parent 9fc602f commit f4a45d9

17 files changed

Lines changed: 235 additions & 54 deletions

graviola-bench/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ openssl = "0.10"
3636
[features]
3737
__bench_openssl = []
3838
__bench_codspeed = ["dep:codspeed-criterion-compat"]
39+
__bench_cycles = []
3940

4041
[[bench]]
4142
name = "x25519"

graviola-bench/benches/aes_gcm.rs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
mod criterion;
2-
use criterion::{BenchmarkId, Criterion, Throughput, criterion_group, criterion_main};
2+
use criterion::{
3+
BenchmarkId, Criterion, CustomMeasurement, Throughput, criterion_group, criterion_main,
4+
};
35

46
fn test_ring_aes_gcm(key: &ring::aead::LessSafeKey, nonce: &[u8; 12], aad: &[u8], plain: &[u8]) {
57
let mut ct = plain.to_vec();
@@ -46,7 +48,7 @@ fn test_graviola_aes_gcm(key: &graviola::aead::AesGcm, nonce: &[u8; 12], aad: &[
4648
key.encrypt(nonce, aad, &mut ct, &mut tag);
4749
}
4850

49-
fn aes128_gcm(c: &mut Criterion) {
51+
fn aes128_gcm(c: &mut Criterion<CustomMeasurement>) {
5052
let key = [0u8; 16];
5153
let nonce = [0u8; 12];
5254
let aad = [0u8; 32];
@@ -92,7 +94,7 @@ fn aes128_gcm(c: &mut Criterion) {
9294
}
9395
}
9496

95-
fn aes256_gcm(c: &mut Criterion) {
97+
fn aes256_gcm(c: &mut Criterion<CustomMeasurement>) {
9698
let key = [0u8; 32];
9799
let nonce = [0u8; 12];
98100
let aad = [0u8; 32];
@@ -138,5 +140,5 @@ fn aes256_gcm(c: &mut Criterion) {
138140
}
139141
}
140142

141-
criterion_group!(benches, aes128_gcm, aes256_gcm);
143+
custom_benchmark_group!(benches, aes128_gcm, aes256_gcm);
142144
criterion_main!(benches);

graviola-bench/benches/chacha20poly1305.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
mod criterion;
2+
use crate::criterion::CustomMeasurement;
23
use criterion::{BenchmarkId, Criterion, Throughput, criterion_group, criterion_main};
34

45
fn test_ring_chacha(key: &ring::aead::LessSafeKey, nonce: &[u8; 12], aad: &[u8], plain: &[u8]) {
@@ -43,7 +44,7 @@ fn test_graviola_chacha(
4344
key.encrypt(nonce, aad, &mut ct, &mut tag);
4445
}
4546

46-
fn bench_chacha20poly1305(c: &mut Criterion) {
47+
fn bench_chacha20poly1305(c: &mut Criterion<CustomMeasurement>) {
4748
let key = [0u8; 32];
4849
let nonce = [0u8; 12];
4950
let aad = [0u8; 32];
@@ -89,5 +90,5 @@ fn bench_chacha20poly1305(c: &mut Criterion) {
8990
}
9091
}
9192

92-
criterion_group!(benches, bench_chacha20poly1305);
93+
custom_benchmark_group!(benches, bench_chacha20poly1305);
9394
criterion_main!(benches);
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
use criterion::Throughput;
2+
use criterion::measurement::{Measurement, ValueFormatter};
3+
use std::arch::asm;
4+
5+
// Measurement plug-in for Criterion that measures elapsed CPU cycles
6+
// by reading the ARM counter-timer hardware register.
7+
8+
pub struct CycleCount;
9+
10+
impl Measurement for CycleCount {
11+
type Intermediate = u64;
12+
type Value = u64;
13+
14+
fn start(&self) -> Self::Intermediate {
15+
self.get_current()
16+
}
17+
18+
fn end(&self, i: Self::Intermediate) -> Self::Value {
19+
self.get_current() - i
20+
}
21+
22+
fn add(&self, v1: &Self::Value, v2: &Self::Value) -> Self::Value {
23+
v1 + v2
24+
}
25+
26+
fn zero(&self) -> Self::Value {
27+
0
28+
}
29+
30+
fn to_f64(&self, value: &Self::Value) -> f64 {
31+
*value as _
32+
}
33+
34+
fn formatter(&self) -> &dyn ValueFormatter {
35+
&CycleCountFormatter
36+
}
37+
}
38+
39+
impl CycleCount {
40+
fn get_current(&self) -> u64 {
41+
let mut value = 0;
42+
unsafe {
43+
asm!(
44+
"isb",
45+
"mrs {destination}, CNTVCT_EL0",
46+
"isb",
47+
destination = inout(reg) value,
48+
)
49+
}
50+
value
51+
}
52+
}
53+
54+
struct CycleCountFormatter;
55+
56+
impl ValueFormatter for CycleCountFormatter {
57+
fn scale_values(&self, _typical_value: f64, _values: &mut [f64]) -> &'static str {
58+
"cycle"
59+
}
60+
61+
fn scale_throughputs(
62+
&self,
63+
_typical_value: f64,
64+
_throughput: &Throughput,
65+
_values: &mut [f64],
66+
) -> &'static str {
67+
"elem/cycle"
68+
}
69+
70+
fn scale_for_machines(&self, _values: &mut [f64]) -> &'static str {
71+
"cycle"
72+
}
73+
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,100 @@
1+
#[cfg(not(all(feature = "__bench_cycles", target_arch = "aarch64")))]
2+
use std::time::{Duration, Instant};
3+
14
#[cfg(feature = "__bench_codspeed")]
25
pub use codspeed_criterion_compat::*;
36
#[cfg(not(feature = "__bench_codspeed"))]
47
pub use criterion::*;
8+
9+
#[cfg(all(feature = "__bench_cycles", target_arch = "aarch64"))]
10+
mod aarch64;
11+
12+
// A measurement type for use with Criterion.
13+
// Depending on the features enabled, it wraps either Criterion's
14+
// standard `WallTime` metric or a target-specific measurement.
15+
pub struct CustomMeasurement(
16+
#[cfg(not(all(feature = "__bench_cycles", target_arch = "aarch64")))] measurement::WallTime,
17+
#[cfg(all(feature = "__bench_cycles", target_arch = "aarch64"))] aarch64::CycleCount,
18+
);
19+
20+
impl measurement::Measurement for CustomMeasurement {
21+
#[cfg(not(all(feature = "__bench_cycles", target_arch = "aarch64")))]
22+
type Intermediate = Instant;
23+
24+
#[cfg(not(all(feature = "__bench_cycles", target_arch = "aarch64")))]
25+
type Value = Duration;
26+
27+
#[cfg(all(feature = "__bench_cycles", target_arch = "aarch64"))]
28+
type Intermediate = u64;
29+
30+
#[cfg(all(feature = "__bench_cycles", target_arch = "aarch64"))]
31+
type Value = u64;
32+
33+
fn start(&self) -> Self::Intermediate {
34+
self.0.start()
35+
}
36+
37+
fn end(&self, i: Self::Intermediate) -> Self::Value {
38+
self.0.end(i)
39+
}
40+
41+
fn add(&self, v1: &Self::Value, v2: &Self::Value) -> Self::Value {
42+
self.0.add(v1, v2)
43+
}
44+
45+
fn zero(&self) -> Self::Value {
46+
self.0.zero()
47+
}
48+
49+
fn to_f64(&self, value: &Self::Value) -> f64 {
50+
self.0.to_f64(value)
51+
}
52+
53+
fn formatter(&self) -> &dyn measurement::ValueFormatter {
54+
self.0.formatter()
55+
//&CustomMeasurementFormatter
56+
}
57+
}
58+
59+
impl CustomMeasurement {
60+
#[allow(dead_code)]
61+
#[cfg(not(all(feature = "__bench_cycles", target_arch = "aarch64")))]
62+
pub fn new() -> Self {
63+
CustomMeasurement(measurement::WallTime)
64+
}
65+
66+
#[allow(dead_code)]
67+
#[cfg(all(feature = "__bench_cycles", target_arch = "aarch64"))]
68+
pub fn new() -> Self {
69+
CustomMeasurement(aarch64::CycleCount)
70+
}
71+
}
72+
73+
/*
74+
struct CustomMeasurementFormatter;
75+
76+
impl measurement::ValueFormatter for CustomMeasurementFormatter {
77+
fn scale_values(&self, _typical_value: f64, _values: &mut [f64]) -> &'static str {
78+
"cycle"
79+
}
80+
81+
fn scale_throughputs(&self, _typical_value: f64, _throughput: &Throughput, _values: &mut [f64]) -> &'static str {
82+
"elem/sycle"
83+
}
84+
85+
fn scale_for_machines(&self, _values: &mut [f64]) -> &'static str {
86+
"cycle"
87+
}
88+
}
89+
*/
90+
91+
#[macro_export]
92+
macro_rules! custom_benchmark_group {
93+
($name:ident, $( $target:path ),+ $(,)*) => {
94+
criterion_group!{
95+
name = $name;
96+
config = Criterion::default().with_measurement(CustomMeasurement::new());
97+
targets = $( $target ),+
98+
}
99+
}
100+
}

graviola-bench/benches/ed25519.rs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
mod criterion;
22
use std::hint::black_box;
33

4+
use crate::criterion::CustomMeasurement;
45
use criterion::{Criterion, Throughput, criterion_group, criterion_main};
56

67
// Generate a fresh key and make a PKCS8 document of it.
7-
fn keygen(c: &mut Criterion) {
8+
fn keygen(c: &mut Criterion<CustomMeasurement>) {
89
let mut group = c.benchmark_group("ed25519-keygen");
910
group.throughput(Throughput::Elements(1));
1011

@@ -53,7 +54,7 @@ fn keygen(c: &mut Criterion) {
5354
});
5455
}
5556

56-
fn verify(c: &mut Criterion) {
57+
fn verify(c: &mut Criterion<CustomMeasurement>) {
5758
let mut group = c.benchmark_group("ed25519-verify");
5859
group.throughput(Throughput::Elements(1));
5960

@@ -102,7 +103,7 @@ fn verify(c: &mut Criterion) {
102103
});
103104
}
104105

105-
fn sign(c: &mut Criterion) {
106+
fn sign(c: &mut Criterion<CustomMeasurement>) {
106107
let mut group = c.benchmark_group("ed25519-sign");
107108
group.throughput(Throughput::Elements(1));
108109
let message = b"\x31\x32\x33\x34\x30\x30";
@@ -151,5 +152,5 @@ fn sign(c: &mut Criterion) {
151152
});
152153
}
153154

154-
criterion_group!(benches, keygen, verify, sign);
155+
custom_benchmark_group!(benches, keygen, verify, sign);
155156
criterion_main!(benches);

graviola-bench/benches/hmac.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
mod criterion;
22
use std::hint::black_box;
33

4+
use crate::criterion::CustomMeasurement;
45
use criterion::{BenchmarkId, Criterion, Throughput, criterion_group, criterion_main};
56
use hmac::Mac;
67

@@ -60,7 +61,7 @@ fn test_graviola_sha384(key: &[u8], data: &[u8]) {
6061
black_box(ctx.finish());
6162
}
6263

63-
fn sha256(c: &mut Criterion) {
64+
fn sha256(c: &mut Criterion<CustomMeasurement>) {
6465
let mut group = c.benchmark_group("hmac-sha256");
6566
let key = [0xff; 48];
6667

@@ -89,7 +90,7 @@ fn sha256(c: &mut Criterion) {
8990
}
9091
}
9192

92-
fn sha384(c: &mut Criterion) {
93+
fn sha384(c: &mut Criterion<CustomMeasurement>) {
9394
let mut group = c.benchmark_group("hmac-sha384");
9495
let key = [0xff; 48];
9596

@@ -118,5 +119,5 @@ fn sha384(c: &mut Criterion) {
118119
}
119120
}
120121

121-
criterion_group!(benches, sha256, sha384);
122+
custom_benchmark_group!(benches, sha256, sha384);
122123
criterion_main!(benches);

graviola-bench/benches/mlkem768.rs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
mod criterion;
22
use std::hint::black_box;
33

4-
use criterion::{Criterion, Throughput, criterion_group, criterion_main};
4+
use criterion::{Criterion, CustomMeasurement, Throughput, criterion_group, criterion_main};
55
use rand_core::RngCore;
66

77
// client first operation
8-
fn mlkem768_keygen(c: &mut Criterion) {
8+
fn mlkem768_keygen(c: &mut Criterion<CustomMeasurement>) {
99
let mut group = c.benchmark_group("mlkem768-keygen");
1010
group.throughput(Throughput::Elements(1));
1111

@@ -46,7 +46,7 @@ fn mlkem768_keygen(c: &mut Criterion) {
4646
}
4747

4848
// server operation
49-
fn mlkem768_encaps(c: &mut Criterion) {
49+
fn mlkem768_encaps(c: &mut Criterion<CustomMeasurement>) {
5050
let mut group = c.benchmark_group("mlkem768-encaps");
5151
group.throughput(Throughput::Elements(1));
5252

@@ -116,7 +116,7 @@ fn mlkem768_encaps(c: &mut Criterion) {
116116
}
117117

118118
// client second operation
119-
fn mlkem768_decaps(c: &mut Criterion) {
119+
fn mlkem768_decaps(c: &mut Criterion<CustomMeasurement>) {
120120
let mut group = c.benchmark_group("mlkem768-decaps");
121121
group.throughput(Throughput::Elements(1));
122122

@@ -178,7 +178,7 @@ fn mlkem768_decaps(c: &mut Criterion) {
178178
}
179179

180180
// combined operation: keygen, encaps-encode, encaps-decode, encaps, decaps
181-
fn mlkem768_combined(c: &mut Criterion) {
181+
fn mlkem768_combined(c: &mut Criterion<CustomMeasurement>) {
182182
let mut group = c.benchmark_group("mlkem768-combined");
183183
group.throughput(Throughput::Elements(1));
184184

@@ -242,7 +242,7 @@ fn mlkem768_combined(c: &mut Criterion) {
242242
});
243243
}
244244

245-
criterion_group!(
245+
custom_benchmark_group!(
246246
benches,
247247
mlkem768_keygen,
248248
mlkem768_encaps,

0 commit comments

Comments
 (0)