Skip to main content
Optimizing Leo programs is essential for reducing proof generation time, memory usage, and on-chain costs. This guide covers compiler optimizations, manual optimization techniques, and best practices for writing efficient zero-knowledge programs.

Understanding Performance Metrics

Key Metrics

Circuit Size: Number of R1CS constraints in the compiled program
  • Directly proportional to proving time
  • Each multiplication creates one constraint
  • Target: Minimize constraints while maintaining functionality
Proving Time: Time to generate a zero-knowledge proof
  • Scales roughly linearly with circuit size
  • Can range from milliseconds to minutes
  • Affected by: Circuit size, hardware (CPU, RAM)
Memory Usage: RAM required during proof generation
  • Scales with circuit size
  • Large circuits may require 8GB+ RAM
  • Out-of-memory errors occur with very large circuits
Verification Time: Time to verify a proof
  • Constant (~10ms) regardless of circuit size
  • This is the power of zk-SNARKs!
Focus optimization efforts on circuit size as it directly impacts proving time and memory usage. Verification is already fast.

Automatic Compiler Optimizations

The Leo compiler applies several optimization passes automatically. Understanding these helps you write code that optimizes well.

1. Constant Propagation

What It Does: Evaluates constant expressions at compile time. Example:
Impact: Eliminates all multiplication constraints from constant computations. How to Leverage:
  • Use const for compile-time constants
  • Perform setup computations with constants when possible
  • Let the compiler fold constants rather than pre-computing manually

2. Loop Unrolling

What It Does: Expands loops with constant bounds into sequential statements. Example:
Impact: Enables further optimizations on loop bodies. No runtime loop overhead. Best Practices:

3. Dead Code Elimination (DCE)

What It Does: Removes unused variables and computations. Example:
Impact: Can dramatically reduce circuit size by removing unnecessary constraints. Statistics: The compiler tracks DCE effectiveness in leo-compiler/src/compiler.rs:244-246:

4. Common Subexpression Elimination (CSE)

What It Does: Reuses computed values instead of recomputing. Example:
Impact: Each eliminated multiplication saves one constraint. Manual Optimization:

5. Function Inlining

What It Does: Replaces inline function calls with the function body. Example:
Tradeoffs:
  • Pro: Eliminates function call overhead, enables further optimization
  • Con: Can increase code size if function is called many times
Guidelines:

6. Static Single Assignment (SSA)

What It Does: Transforms code so each variable is assigned exactly once. Why It Matters: Enables aggressive optimization by making data flow explicit. Example:
Impact: SSA is required for constant propagation, DCE, and CSE to work effectively.
The compiler applies SSA automatically multiple times during compilation (leo-compiler/src/compiler.rs:221-240). You don’t need to write in SSA form, but understanding it helps explain optimization behavior.

Manual Optimization Techniques

Minimize Multiplications

Each multiplication creates one R1CS constraint. Additions are free.
Algebraic Optimizations:

Choose the Right Data Structures

Arrays vs Repeated Variables

Array Indexing Cost:
  • Static index: Free (compiler resolves at compile time)
  • Dynamic index: O(n) constraints where n = array size

Structs vs Tuples

Both have similar performance, choose for readability:

Optimize Cryptographic Operations

Hash Function Selection

Hash Function Comparison:

Commitment Schemes

Minimize Branching Overhead

In zero-knowledge circuits, both branches of a conditional are evaluated.
Ternary Operator Cost: ~3 constraints Optimization Pattern:

Batch Operations

When possible, batch similar operations together:

Avoid Redundant Validations

Hoist Loop-Invariant Code

Move computations that don’t change between iterations outside the loop:
The compiler doesn’t automatically hoist loop-invariant code (loops are unrolled first). You must manually move invariant computations outside loops.

Advanced Optimization Patterns

Precomputed Tables

For expensive operations on small domains, use lookup tables:

Lazy Evaluation

Defer expensive computations until necessary:
Reality Check: Due to flattening, true lazy evaluation is limited in ZK circuits. Focus on:
  1. Making both branches efficient
  2. Ensuring one branch is trivial when possible
  3. Restructuring algorithms to avoid branching on expensive operations

Algebraic Optimizations

Use mathematical identities to reduce operations:

Profiling and Measurement

Enable Compiler Statistics

The compiler tracks optimization effectiveness:

Measure Circuit Size

Count the constraints in generated Aleo code:

Benchmark Proving Time

Best Practices Checklist

Algorithm Design

  • Use algorithms with minimal multiplications
  • Avoid dynamic loops (use fixed iteration counts)
  • Prefer iterative over recursive approaches
  • Batch similar operations together
  • Use lookup tables for small domains

Data Types

  • Use appropriate integer sizes (u8 for small values, not u128)
  • Prefer static array indexing over dynamic
  • Use structs for clarity, tuples for brevity (equivalent performance)
  • Avoid unnecessarily large arrays

Cryptography

  • Use Poseidon2/Poseidon4/Poseidon8 for hashing
  • Use BHP for commitments
  • Avoid SHA-256 unless required for compatibility
  • Batch cryptographic operations when possible

Control Flow

  • Minimize conditional branches
  • Make one branch trivial when possible
  • Hoist loop-invariant code outside loops
  • Unroll small loops (automatically done by compiler)

Code Organization

  • Use inline for small, frequently called functions
  • Use const for compile-time constants
  • Extract common subexpressions into variables
  • Remove dead code and unused variables

Testing and Validation

  • Profile before and after optimizations
  • Measure circuit size (statement count)
  • Benchmark proving time
  • Verify correctness after each optimization

Common Optimization Mistakes

1. Premature Optimization

Rule: Profile first, optimize bottlenecks second.

2. Over-Inlining

3. Ignoring Algorithmic Complexity

Rule: Algorithmic improvements always beat micro-optimizations.

4. Unnecessary Precision

Rule: Use the smallest integer type that fits your value range.

Optimization Workflow

  1. Implement: Write clear, correct code first
  2. Profile: Measure circuit size and proving time
  3. Identify: Find bottlenecks (expensive operations, large loops)
  4. Optimize: Apply targeted optimizations
  5. Measure: Verify improvement
  6. Repeat: Iterate until performance goals are met
Always verify correctness after optimizations. Use comprehensive test cases to ensure optimized code produces the same results as the original.

Further Reading