Skip to main content
Version: 4.0.0

Cycle Tracking

When writing a program, it is useful to know how many RISC-V cycles a portion of the program takes to identify potential performance bottlenecks. SP1 provides a way to track the number of cycles spent in a portion of the program.

Tracking Cycles

Using Print Annotations

For simple debugging, use these annotations to log cycle counts to stdout:

#![no_main]
sp1_zkvm::entrypoint!(main);

fn main() {
let mut nums = vec![1, 1];

// Compute the sum of the numbers.
println!("cycle-tracker-start: compute");
let sum: u64 = nums.iter().sum();
println!("cycle-tracker-end: compute");
}

With this code, you will see output like the following in your logs:

[INFO] compute: 1234 cycles

Using Report Annotations

To store cycle counts across multiple invocations in the ExecutionReport, use the report annotations:

#![no_main]
sp1_zkvm::entrypoint!(main);

fn main() {
// Track cycles across multiple computations
for i in 0..10 {
println!("cycle-tracker-report-start: compute");
expensive_computation(i);
println!("cycle-tracker-report-end: compute");
}
}

Access total cycles from all invocations

let report = client.execute(ELF, &stdin).run().unwrap();
let total_compute_cycles = report.cycle_tracker.get("compute").unwrap();

Using the Cycle Tracker Macro

Add sp1-derive to your dependencies:

sp1-derive = "4.0.0"

Then annotate your functions:

#[sp1_derive::cycle_tracker]
pub fn expensive_function(x: usize) -> usize {
let mut y = 1;
for _ in 0..100 {
y *= x;
y %= 7919;
}
y
}

Profiling a zkVM program

Profiling a zkVM program produces a useful visualization (example profile) which makes it easy to examine program performance and see exactly where VM cycles are being spent without needing to modify the program at all.

To profile a program, you need to:

  1. Enable the profiling feature for sp1-sdk in script/Cargo.toml
  2. Set the env variable TRACE_FILE=trace.json and then call ProverClient::execute() in your script.

If you're executing a larger program (>100M cycles), you should set TRACE_SAMPLE_RATE to reduce the size of the trace file. A sample rate of 1000 means that 1 in every 1000 VM cycles is sampled. By default, every cycle is sampled.

Many examples can be found in the repo, such as this 'fibonacci' script.

Once you have your script it should look like the following:

    // Execute the program using the `ProverClient.execute` method, without generating a proof.
let (_, report) = client.execute(ELF, &stdin).run().unwrap();

As well you must enable the profiling feature on the SDK:

    sp1-sdk = { version = "4.0.0", features = ["profiling"] }

The TRACE_FILE env var tells the executor where to save the profile, and the TRACE_SAMPLE_RATE env var tells the executor how often to sample the program. A larger sample rate will give you a smaller profile, it is the number of instructions in between each sample.

The full command to profile should look something like this

    TRACE_FILE=output.json TRACE_SAMPLE_RATE=100 cargo run ...

To view these profiles, we recommend Samply.

    cargo install --locked samply
samply load output.json

Samply uses the Firefox profiler to create a nice visualization of your programs execution. An example screenshot of the Firefox Profiler

Interpreting the Profile

  • The "time" measurement in the profiler is actually the number of cycles spent, in general the less cycles for a given callframe the better.

  • The CPU usage of the program will always be constant, as its running in the VM which is single threaded.