Using the zkVM
Benchmarking

Benchmark Host and Guest program

Prerequisite

Before you start, please install the latest Nexus zkVM and create a Nexus host project (opens in a new tab).

This benchmark demonstrates the performance of Nexus zkVM using a Fibonacci sequence calculation as an example. We'll analyze both the guest program's RISC-V cycle count and the host program's execution time for various Fibonacci sequence lengths.

After running cargo nexus host benchmark, a new Rust benchmark project directory is created with the following structure:

./benchmark
├── Cargo.lock
├── Cargo.toml
└── src
    ├── guest
    │   ├── Cargo.toml
    │   ├── rust-toolchain.toml
    │   └── src
    │       └── main.rs
    └── main.rs

The guest program is located in src/guest/src/main.rs, and the host program is located in src/main.rs.

Host program

Let's modify the host program src/main.rs as follows:

use nexus_sdk::{
    compile::CompileOpts,
    nova::seq::{Generate, Nova, PP},
    Local, Prover, Verifiable,
};
 
const PACKAGE: &str = "guest";
 
fn generate_pp_and_compile() -> (PP, Nova<Local>) {
    let pp: PP = PP::generate().expect("failed to generate parameters");
    let opts = CompileOpts::new(PACKAGE);
 
    let prover: Nova<Local> = Nova::compile(&opts).expect("failed to compile guest program");
 
    (pp, prover)
}
 
fn prove_execution(pp: &PP, prover: Nova<Local>) -> nexus_sdk::nova::seq::Proof {
    prover.prove(pp).expect("failed to prove program")
}
 
fn verify_execution(pp: &PP, proof: &nexus_sdk::nova::seq::Proof) {
    proof.verify(pp).expect("failed to verify proof");
}
 
fn main() {
    use std::time::Instant;
 
    let start = Instant::now();
    let (pp, prover) = generate_pp_and_compile();
    let duration = start.elapsed();
    println!(
        "Time taken to generate PP and compile: {:.2} seconds",
        duration.as_secs_f64()
    );
 
    let start = Instant::now();
    let proof = prove_execution(&pp, prover);
    let duration = start.elapsed();
    println!(
        "Time taken to prove execution: {:.2} seconds",
        duration.as_secs_f64()
    );
 
    let start = Instant::now();
    verify_execution(&pp, &proof);
    let duration = start.elapsed();
    println!(
        "Time taken to verify execution: {:.2} seconds",
        duration.as_secs_f64()
    );
}

Guest program

And the guest program in src/guest/src/main.rs is as follows:

#![no_std]
#![no_main]
 
#[nexus_rt::profile]
fn fibonacci(n: u32) -> u32 {
    fib(n)
}
 
fn fib(n: u32) -> u32 {
    match n {
        0 => 0,
        1 => 1,
        _ => fib(n - 1) + fib(n - 2),
    }
}
 
#[nexus_rt::main]
fn main() {
    let n = 5;
    assert_eq!(5, fibonacci(n));
}

#[nexus_rt::profile] macro is used to profile a function in the guest program.

Fibonacci benchmark

The results below were obtained in 2024-08 on a MacBook Air M1 with 16 Gb of RAM.

Profile guest RISC-V Cycles count

In the src/guest/ directory, run the command: cargo nexus run 2>/dev/null

Using the #[nexus_rt::profile] macro, the example output from cargo nexus run for the 10th Fibonacci number is:

The 10-th Fibonacci number is: 55
└── Total program cycles: 9928
    └──  'src/guest/src/main.rs:profile': 9750 cycles (98% of total)
n-th FibonacciFibonacci (cycles)% of totalCycle count overheadTotal program cycles
113242%178310
123757%178415
234966%178527
356676%178744
589583%1781073
8144189%1781619
13231692%1782494
21373795%1783915
34603397%1786211
55975098%1789928
891576398%17815941
1442549399%17825671
2334123699%17841414

Profile host execution time

In the root directory, run the command: cargo run --release 2>/dev/null

n-th FibonacciGenerate PP and Compile (seconds)Prove (seconds)Verify (seconds)Total Time (seconds)
120.941.270.7925
127.884.211.3934
228.8210.401.7042
326.2214.671.5143
525.0321.551.6449
824.9933.511.6361
1324.9977.452.03105
2122.6296.931.74123
3425.10145.541.72207
5525.13235.261.73263
8924.83384.731.71412
14425.28629.381.88658
23325.221028.261.781056

Plotting the host and guest results

The graphs below illustrate the relationship between guest RISC-V cycles and host prover time for the Fibonacci sequence calculation.

Key observations:

  1. Total execution time (represented by the number in each bar) includes:
  • Public Parameters Generation
  • Proving
  • Verification
  1. Performance breakdown:
  • Verification time is negligible compared to proving time.
  • Public parameters generation remains consistent at ~25 seconds across all iterations.
  • Proving time increases significantly with the size of the Fibonacci sequence.
  • Guest program optimization is crucial, as RISC-V cycles directly impact overall proving time.

Correlation between RISC-V cycles and Prover time

Suggestions for Developers

Based on the benchmark results, here are some key takeaways and suggestions for developers working with Nexus zkVM:

  1. Optimize Guest Programs: Focus on minimizing RISC-V cycles in your guest programs. The benchmark clearly shows that the number of RISC-V cycles directly impacts overall proving time, which is the most significant component of total execution time.

  2. Consider Algorithm Efficiency: When working on computationally demanding scenarios, prioritize efficient algorithm design and implementation. The Fibonacci sequence example demonstrates how complexity can quickly escalate proving time.

  3. Profile Regularly: Regularly profile your Nexus zkVM projects using tools like #[nexus_rt::profile] to identify performance bottlenecks and opportunities for optimization.