|
| 1 | +// Copyright (C) 2026 Stacks Open Internet Foundation |
| 2 | +// |
| 3 | +// This program is free software: you can redistribute it and/or modify |
| 4 | +// it under the terms of the GNU General Public License as published by |
| 5 | +// the Free Software Foundation, either version 3 of the License, or |
| 6 | +// (at your option) any later version. |
| 7 | +// |
| 8 | +// This program is distributed in the hope that it will be useful, |
| 9 | +// but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 10 | +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 11 | +// GNU General Public License for more details. |
| 12 | +// |
| 13 | +// You should have received a copy of the GNU General Public License |
| 14 | +// along with this program. If not, see <http://www.gnu.org/licenses/>. |
| 15 | + |
| 16 | +//! Benchmarks for the ValueRef zero-copy variable lookup change. |
| 17 | +//! |
| 18 | +//! Compares Epoch33 (old: `lookup_variable` always clones + sanitizes) against |
| 19 | +//! Epoch34 (new: `lookup_variable` returns a borrowed reference for pre-sanitized |
| 20 | +//! epochs, deferring or eliminating the clone entirely). |
| 21 | +//! |
| 22 | +//! Three scenarios exercise the primary beneficiaries: |
| 23 | +//! |
| 24 | +//! 1. `fold_buf_cmp` — fold over a list where each step does `(>= BIG-BUF BIG-BUF)`. |
| 25 | +//! Each step looks up a 128-byte contract constant twice. `special_geq_v2` uses |
| 26 | +//! `as_ref()` throughout, so Epoch34 allocates nothing for the operands. |
| 27 | +//! |
| 28 | +//! 2. `fold_ascii_cmp` — same pattern with a 128-char ASCII string constant. |
| 29 | +//! |
| 30 | +//! 3. `let_local_refs` — a `let` that binds a 128-byte buffer to `x`, then references |
| 31 | +//! `x` N times via `(>= x 0x00)`. Shows the local-variable lookup benefit. |
| 32 | +
|
| 33 | +use std::hint::black_box; |
| 34 | + |
| 35 | +use clarity::vm::contexts::{ContractContext, GlobalContext}; |
| 36 | +use clarity::vm::costs::LimitedCostTracker; |
| 37 | +use clarity::vm::database::MemoryBackingStore; |
| 38 | +use clarity::vm::representations::SymbolicExpression; |
| 39 | +use clarity::vm::types::QualifiedContractIdentifier; |
| 40 | +use clarity::vm::version::ClarityVersion; |
| 41 | +use clarity::vm::{ast, eval_all}; |
| 42 | +use criterion::{BatchSize, BenchmarkId, Criterion, criterion_group, criterion_main}; |
| 43 | +use stacks_common::consts::CHAIN_ID_TESTNET; |
| 44 | +use stacks_common::types::StacksEpochId; |
| 45 | + |
| 46 | +const VERSION: ClarityVersion = ClarityVersion::Clarity2; |
| 47 | + |
| 48 | +// --------------------------------------------------------------------------- |
| 49 | +// Helpers |
| 50 | +// --------------------------------------------------------------------------- |
| 51 | + |
| 52 | +fn parse(source: &str, epoch: StacksEpochId) -> Vec<SymbolicExpression> { |
| 53 | + let contract_id = QualifiedContractIdentifier::transient(); |
| 54 | + let mut cost = LimitedCostTracker::new_free(); |
| 55 | + ast::build_ast(&contract_id, source, &mut cost, VERSION, epoch) |
| 56 | + .expect("failed to parse benchmark program") |
| 57 | + .expressions |
| 58 | +} |
| 59 | + |
| 60 | +/// Execute `parsed` in a fresh environment for `epoch`. |
| 61 | +/// `marf` is provided by the caller (created in `iter_batched` setup) so that |
| 62 | +/// SQLite initialisation is excluded from the timing window. |
| 63 | +fn run(parsed: &[SymbolicExpression], epoch: StacksEpochId, mut marf: MemoryBackingStore) { |
| 64 | + let contract_id = QualifiedContractIdentifier::transient(); |
| 65 | + let db = marf.as_clarity_db(); |
| 66 | + let mut global_context = GlobalContext::new( |
| 67 | + false, |
| 68 | + CHAIN_ID_TESTNET, |
| 69 | + db, |
| 70 | + LimitedCostTracker::new_free(), |
| 71 | + epoch, |
| 72 | + ); |
| 73 | + let mut ctx = ContractContext::new(contract_id, VERSION); |
| 74 | + black_box( |
| 75 | + global_context |
| 76 | + .execute(|g| eval_all(parsed, &mut ctx, g, None)) |
| 77 | + .unwrap(), |
| 78 | + ); |
| 79 | +} |
| 80 | + |
| 81 | +// --------------------------------------------------------------------------- |
| 82 | +// Program generators |
| 83 | +// --------------------------------------------------------------------------- |
| 84 | + |
| 85 | +/// `(fold cmp-step (list 1 … steps) true)` where `cmp-step` does |
| 86 | +/// `(>= BIG-BUF BIG-BUF)` — 2 contract-constant lookups per step. |
| 87 | +fn make_fold_buf_program(steps: usize) -> String { |
| 88 | + let buf_hex = "ab".repeat(128); // 128-byte buffer |
| 89 | + let list_elems = (1..=steps) |
| 90 | + .map(|i| i.to_string()) |
| 91 | + .collect::<Vec<_>>() |
| 92 | + .join(" "); |
| 93 | + format!( |
| 94 | + r#" |
| 95 | +(define-constant BIG-BUF 0x{buf_hex}) |
| 96 | +(define-private (cmp-step (i int) (acc bool)) |
| 97 | + (>= BIG-BUF BIG-BUF)) |
| 98 | +(fold cmp-step (list {list_elems}) true)"# |
| 99 | + ) |
| 100 | +} |
| 101 | + |
| 102 | +/// Same as above but with a 128-char ASCII string constant. |
| 103 | +fn make_fold_ascii_program(steps: usize) -> String { |
| 104 | + let str_content = "a".repeat(128); |
| 105 | + let list_elems = (1..=steps) |
| 106 | + .map(|i| i.to_string()) |
| 107 | + .collect::<Vec<_>>() |
| 108 | + .join(" "); |
| 109 | + format!( |
| 110 | + r#" |
| 111 | +(define-constant BIG-STR "{str_content}") |
| 112 | +(define-private (cmp-step (i int) (acc bool)) |
| 113 | + (>= BIG-STR BIG-STR)) |
| 114 | +(fold cmp-step (list {list_elems}) true)"# |
| 115 | + ) |
| 116 | +} |
| 117 | + |
| 118 | +/// `(let ((x BIG-BUF)) (and (>= x 0x00) … refs times …))` |
| 119 | +/// Measures lookup of a local context variable `refs` times. |
| 120 | +fn make_let_local_program(refs: usize) -> String { |
| 121 | + let buf_hex = "ab".repeat(128); |
| 122 | + let comparisons = (0..refs) |
| 123 | + .map(|_| "(>= x 0x00)".to_string()) |
| 124 | + .collect::<Vec<_>>() |
| 125 | + .join(" "); |
| 126 | + format!( |
| 127 | + r#" |
| 128 | +(define-constant BIG-BUF 0x{buf_hex}) |
| 129 | +(let ((x BIG-BUF)) |
| 130 | + (and {comparisons}))"# |
| 131 | + ) |
| 132 | +} |
| 133 | + |
| 134 | +// --------------------------------------------------------------------------- |
| 135 | +// Benchmark groups |
| 136 | +// --------------------------------------------------------------------------- |
| 137 | + |
| 138 | +fn bench_fold_buf(c: &mut Criterion) { |
| 139 | + let mut group = c.benchmark_group("value_ref/fold_buf_cmp"); |
| 140 | + for &steps in &[50usize, 200] { |
| 141 | + let program = make_fold_buf_program(steps); |
| 142 | + let parsed_33 = parse(&program, StacksEpochId::Epoch33); |
| 143 | + let parsed_34 = parse(&program, StacksEpochId::Epoch34); |
| 144 | + |
| 145 | + group.bench_function(BenchmarkId::new("epoch33", steps), |b| { |
| 146 | + b.iter_batched( |
| 147 | + MemoryBackingStore::new, |
| 148 | + |marf| run(&parsed_33, StacksEpochId::Epoch33, marf), |
| 149 | + BatchSize::SmallInput, |
| 150 | + ); |
| 151 | + }); |
| 152 | + group.bench_function(BenchmarkId::new("epoch34", steps), |b| { |
| 153 | + b.iter_batched( |
| 154 | + MemoryBackingStore::new, |
| 155 | + |marf| run(&parsed_34, StacksEpochId::Epoch34, marf), |
| 156 | + BatchSize::SmallInput, |
| 157 | + ); |
| 158 | + }); |
| 159 | + } |
| 160 | + group.finish(); |
| 161 | +} |
| 162 | + |
| 163 | +fn bench_fold_ascii(c: &mut Criterion) { |
| 164 | + let mut group = c.benchmark_group("value_ref/fold_ascii_cmp"); |
| 165 | + for &steps in &[50usize, 200] { |
| 166 | + let program = make_fold_ascii_program(steps); |
| 167 | + let parsed_33 = parse(&program, StacksEpochId::Epoch33); |
| 168 | + let parsed_34 = parse(&program, StacksEpochId::Epoch34); |
| 169 | + |
| 170 | + group.bench_function(BenchmarkId::new("epoch33", steps), |b| { |
| 171 | + b.iter_batched( |
| 172 | + MemoryBackingStore::new, |
| 173 | + |marf| run(&parsed_33, StacksEpochId::Epoch33, marf), |
| 174 | + BatchSize::SmallInput, |
| 175 | + ); |
| 176 | + }); |
| 177 | + group.bench_function(BenchmarkId::new("epoch34", steps), |b| { |
| 178 | + b.iter_batched( |
| 179 | + MemoryBackingStore::new, |
| 180 | + |marf| run(&parsed_34, StacksEpochId::Epoch34, marf), |
| 181 | + BatchSize::SmallInput, |
| 182 | + ); |
| 183 | + }); |
| 184 | + } |
| 185 | + group.finish(); |
| 186 | +} |
| 187 | + |
| 188 | +fn bench_let_local(c: &mut Criterion) { |
| 189 | + let mut group = c.benchmark_group("value_ref/let_local_refs"); |
| 190 | + for &refs in &[10usize, 50] { |
| 191 | + let program = make_let_local_program(refs); |
| 192 | + let parsed_33 = parse(&program, StacksEpochId::Epoch33); |
| 193 | + let parsed_34 = parse(&program, StacksEpochId::Epoch34); |
| 194 | + |
| 195 | + group.bench_function(BenchmarkId::new("epoch33", refs), |b| { |
| 196 | + b.iter_batched( |
| 197 | + MemoryBackingStore::new, |
| 198 | + |marf| run(&parsed_33, StacksEpochId::Epoch33, marf), |
| 199 | + BatchSize::SmallInput, |
| 200 | + ); |
| 201 | + }); |
| 202 | + group.bench_function(BenchmarkId::new("epoch34", refs), |b| { |
| 203 | + b.iter_batched( |
| 204 | + MemoryBackingStore::new, |
| 205 | + |marf| run(&parsed_34, StacksEpochId::Epoch34, marf), |
| 206 | + BatchSize::SmallInput, |
| 207 | + ); |
| 208 | + }); |
| 209 | + } |
| 210 | + group.finish(); |
| 211 | +} |
| 212 | + |
| 213 | +criterion_group!(benches, bench_fold_buf, bench_fold_ascii, bench_let_local); |
| 214 | +criterion_main!(benches); |
0 commit comments