rework sizing of benchmarks, taking measures at different points

The idea is to get a good sample of measures from running benchmarks
  with various sizes, so one can get an idea of how well a function
  performs at various sizes.

  Given that size can be made arbitrarily large, and that we currently
  report all benchmarks, I installed a fibonacci heuristic to gather
  data points from 0 to the max size using an increasing stepping.

  Defined as a trait as I already anticipate we might need different
  sizing strategy, likely driven by the user via a command-line option;
  but for now, this will do.

Signed-off-by: KtorZ <5680256+KtorZ@users.noreply.github.com>
This commit is contained in:
KtorZ 2025-02-08 18:26:02 +01:00
parent 2dbc33e91f
commit 41440f131b
No known key found for this signature in database
GPG Key ID: 33173CB6F77F4277
3 changed files with 89 additions and 25 deletions

View File

@ -14,7 +14,7 @@ use pallas_primitives::alonzo::{Constr, PlutusData};
use patricia_tree::PatriciaMap; use patricia_tree::PatriciaMap;
use std::{ use std::{
borrow::Borrow, borrow::Borrow,
collections::BTreeMap, collections::{BTreeMap, VecDeque},
convert::TryFrom, convert::TryFrom,
fmt::{Debug, Display}, fmt::{Debug, Display},
ops::Deref, ops::Deref,
@ -506,21 +506,84 @@ pub struct Benchmark {
unsafe impl Send for Benchmark {} unsafe impl Send for Benchmark {}
trait Sizer {
fn is_done(&self) -> bool;
fn next(&mut self) -> usize;
}
struct FibonacciSizer {
max_size: usize,
previous_sizes: VecDeque<usize>,
current_size: usize,
}
impl FibonacciSizer {
fn new(max_size: usize) -> Self {
Self {
max_size,
previous_sizes: VecDeque::new(),
current_size: 1,
}
}
}
impl Sizer for FibonacciSizer {
fn is_done(&self) -> bool {
self.current_size >= self.max_size
}
fn next(&mut self) -> usize {
match self.previous_sizes.len() {
0 => {
self.previous_sizes.push_front(1);
return 0;
}
1 => {
self.previous_sizes.push_front(1);
return 1;
}
_ => self.current_size += self.previous_sizes.pop_back().unwrap(),
}
self.previous_sizes.push_front(self.current_size);
self.current_size.min(self.max_size)
}
}
#[cfg(test)]
mod test_sizer {
use super::{FibonacciSizer, Sizer};
#[test]
pub fn fib_sizer_sequence() {
let mut sizer = FibonacciSizer::new(100);
let mut sizes = Vec::new();
while !sizer.is_done() {
sizes.push(sizer.next())
}
assert_eq!(sizes, vec![0, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 100])
}
}
impl Benchmark { impl Benchmark {
pub const DEFAULT_MAX_SIZE: usize = 10;
pub fn run( pub fn run(
self, self,
seed: u32, seed: u32,
max_iterations: usize, max_size: usize,
plutus_version: &PlutusVersion, plutus_version: &PlutusVersion,
) -> BenchmarkResult { ) -> BenchmarkResult {
let mut measures = Vec::with_capacity(max_iterations); let mut measures = Vec::with_capacity(max_size);
let mut iteration = 0; let mut sizer = FibonacciSizer::new(max_size);
let mut prng = Prng::from_seed(seed); let mut prng = Prng::from_seed(seed);
let mut success = true; let mut success = true;
while success && max_iterations > iteration { while success && !sizer.is_done() {
let size = Data::integer(num_bigint::BigInt::from(iteration as i64)); let size = sizer.next();
let fuzzer = self.sampler.program.apply_data(size); let size_as_data = Data::integer(num_bigint::BigInt::from(size));
let fuzzer = self.sampler.program.apply_data(size_as_data);
match prng.sample(&fuzzer) { match prng.sample(&fuzzer) {
Ok(None) => { Ok(None) => {
@ -529,14 +592,13 @@ impl Benchmark {
Ok(Some((new_prng, value))) => { Ok(Some((new_prng, value))) => {
prng = new_prng; prng = new_prng;
measures.push(self.eval(&value, plutus_version).cost()) measures.push((size, self.eval(&value, plutus_version).cost()))
} }
Err(_e) => { Err(_e) => {
success = false; success = false;
} }
} }
iteration += 1;
} }
BenchmarkResult { BenchmarkResult {
@ -1469,7 +1531,7 @@ impl Assertion<UntypedExpr> {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct BenchmarkResult { pub struct BenchmarkResult {
pub bench: Benchmark, pub bench: Benchmark,
pub measures: Vec<ExBudget>, pub measures: Vec<(usize, ExBudget)>,
pub success: bool, pub success: bool,
} }

View File

@ -50,8 +50,9 @@ impl EventListener for Json {
"measures": result.measures "measures": result.measures
.into_iter() .into_iter()
.map(|measure| serde_json::json!({ .map(|measure| serde_json::json!({
"memory": measure.mem, "size": measure.0,
"cpu": measure.cpu "memory": measure.1.mem,
"cpu": measure.1.cpu
})) }))
.collect::<Vec<_>>() .collect::<Vec<_>>()
})) }))

View File

@ -1,4 +1,4 @@
use aiken_lang::test_framework::PropertyTest; use aiken_lang::test_framework::Benchmark;
use aiken_project::watch::with_project; use aiken_project::watch::with_project;
use rand::prelude::*; use rand::prelude::*;
use std::{ use std::{
@ -17,18 +17,20 @@ pub struct Args {
#[clap(long)] #[clap(long)]
seed: Option<u32>, seed: Option<u32>,
/// How many times we will run each benchmark in the relevant project. /// The maximum size to benchmark with. Note that this does not necessarily equates the number
#[clap(long, default_value_t = PropertyTest::DEFAULT_MAX_SUCCESS)] /// of measurements actually performed but controls the maximum size given to a Sampler.
times_to_run: usize, #[clap(long, default_value_t = Benchmark::DEFAULT_MAX_SIZE)]
max_size: usize,
/// Only run tests if they match any of these strings. /// Only run benchmarks if they match any of these strings.
///
/// You can match a module with `-m aiken/list` or `-m list`. /// You can match a module with `-m aiken/list` or `-m list`.
/// You can match a test with `-m "aiken/list.{map}"` or `-m "aiken/option.{flatten_1}"` /// You can match a test with `-m "aiken/list.{map}"` or `-m "aiken/option.{flatten_1}"`
#[clap(short, long)] #[clap(short, long)]
match_tests: Option<Vec<String>>, match_benchmarks: Option<Vec<String>>,
/// This is meant to be used with `--match-tests`. /// This is meant to be used with `--match-benchmarks`.
/// It forces test names to match exactly /// It forces benchmark names to match exactly
#[clap(short, long)] #[clap(short, long)]
exact_match: bool, exact_match: bool,
@ -39,10 +41,10 @@ pub struct Args {
pub fn exec( pub fn exec(
Args { Args {
directory, directory,
match_tests, match_benchmarks,
exact_match, exact_match,
seed, seed,
times_to_run, max_size,
env, env,
}: Args, }: Args,
) -> miette::Result<()> { ) -> miette::Result<()> {
@ -55,12 +57,11 @@ pub fn exec(
false, false,
!io::stdout().is_terminal(), !io::stdout().is_terminal(),
|p| { |p| {
// We don't want to check here, we want to benchmark
p.benchmark( p.benchmark(
match_tests.clone(), match_benchmarks.clone(),
exact_match, exact_match,
seed, seed,
times_to_run, max_size,
env.clone(), env.clone(),
) )
}, },