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

CategoryFeatures
Typesi8/16/32/64, u8/16/32/64, f32/64, bool, char, String, &str, usize, isize
Variableslet, let mut, const, type inference, shadowing
Functionsfn, closures |x| x + 1, generic functions (basic), impl blocks
Control flowif/else, while, loop, for x in, match (patterns, guards), break, continue
Ownership& references, &mut mutable references, move closures, lifetime annotations (parsed, not enforced)
StructsDefinition, field access, methods, associated functions
EnumsVariants, match exhaustiveness, Option<T>, Result<T, E>
CollectionsVec<T>, HashMap<K, V>, String, Box<T>
Iterators.iter(), .map(), .filter(), .fold(), .collect(), .enumerate(), .zip(), .chain(), .take(), .skip(), .any(), .all(), .find(), .sum(), .count()
TraitsTrait definitions and impl Trait for Type (signatures only)
Macrosprintln!, 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 use imports — all types must be fully qualified or built-in
  • No impl Trait return types
  • No where clauses
  • No unsafe blocks