Rust Energy Analysis
Joule analyzes Rust code with awareness of ownership, iterator chains, and zero-cost abstractions. The lifter models the energy cost of heap allocations, reference counting, and iterator fusion.
Quick Start
# Static energy analysis
joulec --lift rust lib.rs
# Execute with energy tracking
joulec --lift-run rust lib.rs
# Execute with energy optimization
joulec --energy-optimize --lift-run rust lib.rs
Supported Features
| Category | Features |
|---|---|
| Types | i8/16/32/64, u8/16/32/64, f32/64, bool, char, String, &str, usize, isize |
| Variables | let, let mut, const, type inference, shadowing |
| Functions | fn, closures |x| x + 1, generic functions (basic), impl blocks |
| Control flow | if/else, while, loop, for x in, match (patterns, guards), break, continue |
| Ownership | & references, &mut mutable references, move closures, lifetime annotations (parsed, not enforced) |
| Structs | Definition, field access, methods, associated functions |
| Enums | Variants, match exhaustiveness, Option<T>, Result<T, E> |
| Collections | Vec<T>, HashMap<K, V>, String, Box<T> |
| Iterators | .iter(), .map(), .filter(), .fold(), .collect(), .enumerate(), .zip(), .chain(), .take(), .skip(), .any(), .all(), .find(), .sum(), .count() |
| Traits | Trait definitions and impl Trait for Type (signatures only) |
| Macros | println!, format!, vec!, panic! (pattern-matched, not expanded) |
Common Energy Anti-Patterns
1. clone() in Hot Loops
#![allow(unused)] fn main() { // BAD — clones String every iteration (heap allocation + copy) for item in &data { let owned = item.clone(); process(owned); } // GOOD — borrow instead of clone for item in &data { process_ref(item); } }
Category: ALLOCATION | Severity: High | Savings: ~5x
Each .clone() on a String involves malloc + memcpy. At 200 pJ per DRAM access, this dominates in tight loops.
2. Unnecessary collect() in Iterator Chains
#![allow(unused)] fn main() { // BAD — collects into intermediate Vec, then iterates again let filtered: Vec<i32> = data.iter() .filter(|&&x| x > 0) .cloned() .collect(); // allocates intermediate Vec let sum: i32 = filtered.iter().sum(); // GOOD — single iterator chain, no intermediate allocation let sum: i32 = data.iter() .filter(|&&x| x > 0) .sum(); }
Category: ALLOCATION | Severity: Medium | Savings: ~2x
Iterator fusion in Rust is a zero-cost abstraction — the compiler fuses the chain into a single loop. Breaking the chain with .collect() defeats this.
3. Box::new() in Loops
#![allow(unused)] fn main() { // BAD — heap allocation per iteration let mut nodes: Vec<Box<Node>> = Vec::new(); for i in 0..1000 { nodes.push(Box::new(Node { value: i })); } // GOOD — pre-allocate with arena or flat Vec let mut nodes: Vec<Node> = Vec::with_capacity(1000); for i in 0..1000 { nodes.push(Node { value: i }); } }
Category: ALLOCATION | Severity: Medium | Savings: ~3x
4. format!() String Building in Loops
#![allow(unused)] fn main() { // BAD — format! allocates a new String every iteration let mut log = String::new(); for i in 0..1000 { log.push_str(&format!("item {}\n", i)); } // GOOD — write! to a single buffer use std::fmt::Write; let mut log = String::with_capacity(10000); for i in 0..1000 { write!(log, "item {}\n", i).unwrap(); } }
Category: STRING | Severity: Medium | Savings: ~2x
Worked Example
fn process_data(data: &[f64]) -> f64 { let filtered: Vec<f64> = data.iter() .filter(|&&x| x > 0.0) .cloned() .collect(); let normalized: Vec<f64> = filtered.iter() .map(|&x| x / filtered.len() as f64) .collect(); normalized.iter().sum() } fn main() { let data = vec![3.0, -1.0, 4.0, -2.0, 5.0, 1.0, -3.0, 2.0]; let result = process_data(&data); println!("Result: {}", result); }
$ joulec --lift rust pipeline.rs
Energy Analysis: pipeline.rs
process_data 12.30 nJ (confidence: 0.65)
main 2.10 nJ (confidence: 0.90)
Total: 14.40 nJ
Recommendations:
!! [ALLOCATION] process_data — two collect() calls create intermediate Vecs
Suggestion: fuse into a single iterator chain without intermediate allocation
Estimated savings: 2-3x
Optimized version:
data.iter()
.filter(|&&x| x > 0.0)
.map(|&x| x / count as f64)
.sum()
Zero-Cost Abstractions Are Real
Rust's iterator chains compile to the same machine code as hand-written loops. Joule confirms this:
#![allow(unused)] fn main() { // Iterator version fn sum_positive_iter(data: &[i32]) -> i32 { data.iter().filter(|&&x| x > 0).sum() } // Manual loop version fn sum_positive_loop(data: &[i32]) -> i32 { let mut sum = 0; for &x in data { if x > 0 { sum += x; } } sum } }
$ joulec --lift rust zero_cost.rs
sum_positive_iter 4.20 nJ (confidence: 0.70)
sum_positive_loop 4.20 nJ (confidence: 0.70)
Identical energy. The abstraction is truly zero-cost.
Limitations
- No trait dispatch (static or dynamic) — trait bounds are parsed but not resolved
- No lifetime analysis — lifetimes are parsed but not enforced
- No
async/await— async is not supported - No procedural macros — only
println!,format!,vec!,panic!are recognized - No
useimports — all types must be fully qualified or built-in - No
impl Traitreturn types - No
whereclauses - No
unsafeblocks