Skip to main content

energy_bench/
energy_bench.rs

1// Energy consumption benchmark for hash functions (SHA-256, SHA-512, SHA3-256, BLAKE3).
2//
3// Reads RAPL (Running Average Power Limit) energy counters from sysfs to measure
4// CPU energy consumed per byte hashed.  Despite the "intel" naming, the Linux
5// intel_rapl driver also supports AMD Zen processors since kernel 5.4.
6//
7// # Kernel modules / packages required
8//
9// ## Intel
10//
11//     Kernel CONFIG_INTEL_RAPL       (built-in on most distro kernels)
12//     lsmod | grep intel_rapl         # verify it's loaded
13//     modprobe intel_rapl             # load it if missing
14//
15// ## AMD
16//
17//     Kernel CONFIG_INTEL_RAPL       (reused by AMD since kernel 5.4; CONFIG_AMD_RAPL alias)
18//     - or -
19//     Kernel CONFIG_AMD_ENERGY       (separate driver on some older kernels)
20//     lsmod | grep -E 'intel_rapl|amd_energy'
21//     modprobe intel_rapl             # most common
22//
23// ## AMD – MSR prerequisite
24//
25// The powercap interface may also require the `msr` module on AMD:
26//
27//     modprobe msr                    # needed on some AMD configs
28//
29// To verify RAPL is available after loading modules:
30//
31//     ls /sys/class/powercap/intel-rapl:*/
32//
33// ## CPU frequency governor (for stable measurements)
34//
35//     cpupower frequency-set -g performance   # intel
36//     - or -
37//     cpufreq-set -g performance              # AMD (older)
38//     - or -
39//     echo performance > /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor
40//
41//     # cpupower / cpufreq-set packages:
42//     apt install linux-cpupower     # Debian/Ubuntu
43//     dnf install kernel-tools       # Fedora
44//     pacman -S cpupower             # Arch
45//
46// # Running
47//
48//     sudo make run                  # sets governor, pins to core 0, restores after
49//     cargo run -p energy_bench --release
50
51use std::{
52    fs,
53    hint::black_box,
54    path::{Path, PathBuf},
55    thread,
56    time::{Duration, Instant},
57};
58
59use crypto::{
60    Hasher,
61    blake3::Blake3,
62    sha2::{Sha256, Sha512},
63    sha3::Sha3_256,
64};
65
66const CHUNK_SIZE: usize = 1024 * 1024; // 1 MiB
67const BENCH_SECS: u64 = 10;
68const COOLDOWN_SECS: u64 = 30;
69const IDLE_SECS: u64 = 5;
70
71struct RaplReader {
72    paths: Vec<PathBuf>,
73    names: Vec<String>,
74}
75
76impl RaplReader {
77    fn detect() -> Result<Self, String> {
78        let powercap = Path::new("/sys/class/powercap");
79        if !powercap.exists() {
80            return Err("/sys/class/powercap not found. RAPL is not available on this system.".into());
81        }
82
83        let mut paths = Vec::new();
84        let mut names = Vec::new();
85        let entries = fs::read_dir(powercap).map_err(|e| format!("cannot read /sys/class/powercap: {e}"))?;
86
87        for entry in entries {
88            let entry = entry.map_err(|e| format!("error reading powercap entry: {e}"))?;
89            let fname = entry.file_name().to_string_lossy().to_string();
90
91            if fname.starts_with("intel-rapl:") && fname.matches(':').count() == 1 {
92                let energy_path = entry.path().join("energy_uj");
93                if energy_path.exists() {
94                    let name = fs::read_to_string(entry.path().join("name"))
95                        .unwrap_or_default()
96                        .trim()
97                        .to_string();
98                    names.push(name);
99                    paths.push(energy_path);
100                }
101            }
102        }
103
104        if paths.is_empty() {
105            return Err("no intel-rapl energy counters found in /sys/class/powercap.\n\
106                 RAPL is available on Intel CPUs (Sandy Bridge+) and AMD Zen CPUs (kernel 5.4+)."
107                .into());
108        }
109
110        Ok(Self {
111            paths,
112            names,
113        })
114    }
115
116    fn read_energy_uj(&self) -> Result<u64, String> {
117        let mut total: u64 = 0;
118        for path in &self.paths {
119            let s = fs::read_to_string(path).map_err(|e| format!("cannot read {}: {e}", path.display()))?;
120            let val: u64 = s
121                .trim()
122                .parse()
123                .map_err(|e| format!("cannot parse {}: {e}", path.display()))?;
124            total += val;
125        }
126        Ok(total)
127    }
128}
129
130struct Measurement {
131    algo: &'static str,
132    total_bytes: u64,
133    energy_j: f64,
134    wall_secs: f64,
135}
136
137fn measure_hash<H: Hasher>(
138    rapl: &RaplReader,
139    data: &[u8],
140    duration: Duration,
141    algo: &'static str,
142) -> Result<Measurement, String> {
143    let chunk_bytes = data.len() as u64;
144    let mut iterations: u64 = 0;
145
146    let energy_before = rapl.read_energy_uj()?;
147    let wall_start = Instant::now();
148
149    while wall_start.elapsed() < duration {
150        let h = H::hash(black_box(data));
151        black_box(h);
152        iterations += 1;
153    }
154
155    let wall_elapsed = wall_start.elapsed();
156    let energy_after = rapl.read_energy_uj()?;
157    let total_bytes = chunk_bytes * iterations;
158
159    let energy_uj = if energy_after >= energy_before {
160        energy_after - energy_before
161    } else {
162        u64::MAX - energy_before + energy_after + 1
163    };
164
165    Ok(Measurement {
166        algo,
167        total_bytes,
168        energy_j: energy_uj as f64 / 1_000_000.0,
169        wall_secs: wall_elapsed.as_secs_f64(),
170    })
171}
172
173fn measure_idle(rapl: &RaplReader, secs: u64) -> Result<(f64, f64), String> {
174    let before = rapl.read_energy_uj()?;
175    let start = Instant::now();
176    while start.elapsed().as_secs() < secs {
177        std::hint::spin_loop();
178    }
179    let after = rapl.read_energy_uj()?;
180    let elapsed = start.elapsed().as_secs_f64();
181
182    let energy_uj = if after >= before {
183        after - before
184    } else {
185        u64::MAX - before + after + 1
186    };
187
188    Ok((energy_uj as f64 / 1_000_000.0, elapsed))
189}
190
191fn commafy(n: u64) -> String {
192    let s = n.to_string();
193    let mut result = String::new();
194    for (i, c) in s.chars().rev().enumerate() {
195        if i > 0 && i % 3 == 0 {
196            result.push(',');
197        }
198        result.push(c);
199    }
200    result.chars().rev().collect()
201}
202
203fn main() {
204    let rapl = match RaplReader::detect() {
205        Ok(r) => r,
206        Err(e) => {
207            eprintln!("error: {e}");
208            std::process::exit(1);
209        }
210    };
211
212    let governor = fs::read_to_string("/sys/devices/system/cpu/cpu0/cpufreq/scaling_governor")
213        .unwrap_or_default()
214        .trim()
215        .to_string();
216
217    if governor != "performance" {
218        eprintln!(
219            "warning: CPU governor is '{governor}', not 'performance'.\n\
220             run 'make run' or set governor manually for stable measurements.\n"
221        );
222    }
223
224    for (n, p) in rapl.names.iter().zip(rapl.paths.iter()) {
225        eprintln!("found RAPL domain: {} ({})", n, p.display());
226    }
227
228    let data = vec![0xA5u8; CHUNK_SIZE];
229    let bench_duration = Duration::from_secs(BENCH_SECS);
230
231    eprintln!("\nmeasuring idle baseline ({IDLE_SECS}s)...");
232    let (idle_j, idle_secs) = measure_idle(&rapl, IDLE_SECS).unwrap_or_else(|e| {
233        eprintln!("warning: idle measurement failed: {e}");
234        (0.0, IDLE_SECS as f64)
235    });
236    let idle_w = idle_j / idle_secs;
237
238    eprintln!("\nbenchmarking SHA-256 ({BENCH_SECS}s)...");
239    let sha = measure_hash::<Sha256>(&rapl, &data, bench_duration, "SHA-256").unwrap_or_else(|e| {
240        eprintln!("error: {e}");
241        std::process::exit(1);
242    });
243
244    eprintln!("cooling down ({COOLDOWN_SECS}s)...");
245    thread::sleep(Duration::from_secs(COOLDOWN_SECS));
246
247    eprintln!("\nbenchmarking BLAKE3 ({BENCH_SECS}s)...");
248    let blake3 = measure_hash::<Blake3>(&rapl, &data, bench_duration, "BLAKE3").unwrap_or_else(|e| {
249        eprintln!("error: {e}");
250        std::process::exit(1);
251    });
252
253    eprintln!("cooling down ({COOLDOWN_SECS}s)...");
254    thread::sleep(Duration::from_secs(COOLDOWN_SECS));
255
256    eprintln!("\nbenchmarking SHA-512 ({BENCH_SECS}s)...");
257    let sha512 = measure_hash::<Sha512>(&rapl, &data, bench_duration, "SHA-512").unwrap_or_else(|e| {
258        eprintln!("error: {e}");
259        std::process::exit(1);
260    });
261
262    eprintln!("cooling down ({COOLDOWN_SECS}s)...");
263    thread::sleep(Duration::from_secs(COOLDOWN_SECS));
264
265    eprintln!("\nbenchmarking SHA3-256 ({BENCH_SECS}s)...");
266    let sha3 = measure_hash::<Sha3_256>(&rapl, &data, bench_duration, "SHA3-256").unwrap_or_else(|e| {
267        eprintln!("error: {e}");
268        std::process::exit(1);
269    });
270
271    println!();
272    println!(
273        "idle power: {:.1} W  |  governor: {}  |  bench duration: {}s",
274        idle_w, governor, BENCH_SECS,
275    );
276    println!();
277
278    let mut results = vec![&sha, &sha512, &sha3, &blake3];
279    results.sort_by(|a, b| {
280        let a_ratio = a.total_bytes as f64 / a.energy_j;
281        let b_ratio = b.total_bytes as f64 / b.energy_j;
282        b_ratio.partial_cmp(&a_ratio).unwrap()
283    });
284
285    println!(
286        "{:<12} {:>16} {:>12} {:>16} {:>16} {:>12}",
287        "Algorithm", "Bytes Hashed", "Energy (J)", "bytes/J", "J/byte", "Thruput"
288    );
289    println!("{:-<12} {:-<16} {:-<12} {:-<16} {:-<16} {:-<12}", "", "", "", "", "", "");
290
291    for m in &results {
292        let b_per_j = m.total_bytes as f64 / m.energy_j;
293        let j_per_b = m.energy_j / m.total_bytes as f64;
294        let thr_mb_s = m.total_bytes as f64 / m.wall_secs / 1_048_576.0;
295
296        println!(
297            "{:<12} {:>16} {:>11.1} J {:>16} {:>15.9e} {:>11.1} MB/s",
298            m.algo,
299            commafy(m.total_bytes),
300            m.energy_j,
301            commafy(b_per_j as u64),
302            j_per_b,
303            thr_mb_s,
304        );
305    }
306}