Energy Optimization Walkthrough

This walkthrough takes a real Python program through the full Joule energy analysis and optimization pipeline — from first scan to CI-ready energy budgets.

Step 1: Start with Real Code

Here's a data processing program with several common energy anti-patterns:

def find_duplicates(items, reference):
    """Find items that appear in both lists."""
    duplicates = []
    for item in items:
        for ref in reference:          # nested loop: O(n*m)
            if item == ref:
                duplicates.append(item)
    return duplicates

def build_report(records):
    """Build a text report from records."""
    report = ""
    for i in range(len(records)):      # len() in loop, string concat
        report += "Record " + str(i) + ": " + str(records[i]) + "\n"
    return report

def process_batch(data):
    """Filter and transform a data batch."""
    results = []
    for item in data:
        temp = []                      # allocation inside loop
        temp.append(item * 2)
        if temp[0] > 10:
            results.append(temp[0])
    return results

def search_all(items, targets):
    """Check if all targets exist in items."""
    found = 0
    for t in targets:
        for item in items:             # linear scan for each target
            if item == t:
                found = found + 1
                # no break — scans entire list even after finding match
    return found

def main():
    data = []
    for i in range(500):
        data.append(i)

    reference = []
    for i in range(250, 750):
        reference.append(i)

    dups = find_duplicates(data, reference)
    report = build_report(data)
    processed = process_batch(data)
    count = search_all(data, reference)

    print(len(dups))
    print(len(processed))
    print(count)

main()

Step 2: Run Baseline Analysis

$ joulec --lift python anti_patterns.py --energy-report baseline.json
Energy Analysis: anti_patterns.py

  find_duplicates    285.00 nJ  (confidence: 0.50)
  build_report        72.50 nJ  (confidence: 0.55)
  process_batch       18.30 nJ  (confidence: 0.60)
  search_all         285.00 nJ  (confidence: 0.50)
  main                12.40 nJ  (confidence: 0.75)

  Total: 673.20 nJ

Step 3: Read the Recommendations

Recommendations:

  !!! [ALGORITHM] find_duplicates — O(n^2) nested loop for membership test
      Suggestion: convert reference to a set for O(1) lookups
      Estimated savings: 50x

  !!! [ALGORITHM] search_all — O(n^2) nested loop for membership test
      Suggestion: convert items to a set for O(1) lookups
      Estimated savings: 50x

  !! [STRING] build_report — string concatenation in loop
      Suggestion: use "".join() to build string in one allocation
      Estimated savings: 8x

  !! [LOOP] search_all — no early exit after finding match
      Suggestion: add break after match to avoid scanning remaining elements
      Estimated savings: 2x (average case)

  !  [REDUNDANCY] build_report — len(records) called in loop range
      Suggestion: compute len() once before the loop
      Estimated savings: 1.2x

  !  [ALLOCATION] process_batch — list allocation inside loop body
      Suggestion: reuse buffer or eliminate temporary list
      Estimated savings: 3x

  .  [REDUNDANCY] build_report — str() conversion could use f-string
      Suggestion: use f"Record {i}: {records[i]}" for cleaner concatenation
      Estimated savings: 1.1x

Severity markers: !!! Critical, !! High, ! Medium, . Low

Step 4: Fix Critical Issues First

Fix #1: Hash set for find_duplicates

def find_duplicates(items, reference):
    ref_set = set(reference)           # O(m) one-time cost
    duplicates = []
    for item in items:
        if item in ref_set:            # O(1) per lookup
            duplicates.append(item)
    return duplicates
$ joulec --lift python fixed_v1.py
  find_duplicates    8.20 nJ  (confidence: 0.70)  # was 285.00 nJ — 34x reduction

Fix #2: Hash set for search_all + early tracking

def search_all(items, targets):
    item_set = set(items)
    found = 0
    for t in targets:
        if t in item_set:
            found = found + 1
    return found
$ joulec --lift python fixed_v2.py
  search_all    6.80 nJ  (confidence: 0.75)  # was 285.00 nJ — 42x reduction

Fix #3: String builder for build_report

def build_report(records):
    n = len(records)
    parts = []
    for i in range(n):
        parts.append(f"Record {i}: {records[i]}")
    report = "\n".join(parts) + "\n"
    return report
$ joulec --lift python fixed_v3.py
  build_report    9.50 nJ  (confidence: 0.75)  # was 72.50 nJ — 7.6x reduction

Fix #4: Eliminate temporary allocation in process_batch

def process_batch(data):
    results = []
    for item in data:
        doubled = item * 2
        if doubled > 10:
            results.append(doubled)
    return results
$ joulec --lift python fixed_v4.py
  process_batch    6.10 nJ  (confidence: 0.75)  # was 18.30 nJ — 3x reduction

Step 5: Run Optimized Baseline

After all four fixes:

$ joulec --lift python optimized.py --energy-report optimized.json
Energy Analysis: optimized.py

  find_duplicates     8.20 nJ  (confidence: 0.70)
  build_report        9.50 nJ  (confidence: 0.75)
  process_batch       6.10 nJ  (confidence: 0.75)
  search_all          6.80 nJ  (confidence: 0.75)
  main               12.40 nJ  (confidence: 0.75)

  Total: 43.00 nJ

  No recommendations — all detected anti-patterns have been resolved.

Step 6: Apply Automated Optimization

The --energy-optimize flag applies four compiler passes on top of your fixes:

$ joulec --energy-optimize --lift-run python optimized.py
Energy Optimization Report:
  Pass 1 (Thermal-Aware Selection): 2 instructions adapted
  Pass 2 (Branch Optimization):     3 branches reordered
  Pass 3 (Loop Unrolling):          1 loop unrolled (trip count 4)
  Pass 4 (DRAM Layout Analysis):    no suggestions

  Optimized energy: 38.70 nJ (10.0% reduction from automated passes)

Step 7: Compare Results

FunctionBeforeAfter FixesAfter OptimizationReduction
find_duplicates285.00 nJ8.20 nJ7.40 nJ97.4%
build_report72.50 nJ9.50 nJ8.90 nJ87.7%
process_batch18.30 nJ6.10 nJ5.50 nJ69.9%
search_all285.00 nJ6.80 nJ6.10 nJ97.9%
main12.40 nJ12.40 nJ10.80 nJ12.9%
Total673.20 nJ43.00 nJ38.70 nJ94.3%

The manual fixes account for 93.6% of the savings. The automated passes add another 10% on top.

Step 8: Set an Energy Budget for CI

# Set budget at 50 nJ — optimized version passes
$ joulec --lift python optimized.py --energy-budget 50nJ
# Exit code: 0 (within budget)

# The original version would fail
$ joulec --lift python anti_patterns.py --energy-budget 50nJ
# Exit code: 1 (budget exceeded: 673.20 nJ > 50.00 nJ)

GitHub Actions Integration

- name: Energy budget check
  run: |
    joulec --lift python src/core.py --energy-budget 100nJ
    joulec --lift python src/utils.py --energy-budget 50nJ

The build fails if any file exceeds its budget, catching energy regressions before merge.

Step 9: Generate Reports for Dashboards

$ joulec --lift python optimized.py --energy-report report.json

The JSON report includes per-function energy, confidence scores, and any remaining recommendations. Feed this into Grafana, Datadog, or any monitoring system to track energy consumption across releases.

Key Takeaways

  1. Start with --lift to get a baseline without running the code
  2. Fix critical recommendations first — algorithmic changes (O(n^2) → O(n)) yield the biggest savings
  3. Use --energy-optimize for automated passes on top of manual fixes
  4. Set --energy-budget in CI to prevent regressions
  5. Generate --energy-report JSON for tracking trends over time