Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added option to manually specify a reference to compare the results to. #744

Merged
merged 4 commits into from
Jun 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 45 additions & 10 deletions src/benchmark/relative_speed.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,16 @@ pub struct BenchmarkResultWithRelativeSpeed<'a> {
pub result: &'a BenchmarkResult,
pub relative_speed: Scalar,
pub relative_speed_stddev: Option<Scalar>,
pub is_fastest: bool,
pub is_reference: bool,
// Less means faster
pub relative_ordering: Ordering,
}

pub fn compare_mean_time(l: &BenchmarkResult, r: &BenchmarkResult) -> Ordering {
l.mean.partial_cmp(&r.mean).unwrap_or(Ordering::Equal)
}

fn fastest_of(results: &[BenchmarkResult]) -> &BenchmarkResult {
pub fn fastest_of(results: &[BenchmarkResult]) -> &BenchmarkResult {
results
.iter()
.min_by(|&l, &r| compare_mean_time(l, r))
Expand All @@ -24,32 +26,38 @@ fn fastest_of(results: &[BenchmarkResult]) -> &BenchmarkResult {

fn compute_relative_speeds<'a>(
results: &'a [BenchmarkResult],
fastest: &'a BenchmarkResult,
reference: &'a BenchmarkResult,
sort_order: SortOrder,
) -> Vec<BenchmarkResultWithRelativeSpeed<'a>> {
let mut results: Vec<_> = results
.iter()
.map(|result| {
let is_fastest = result == fastest;
let is_reference = result == reference;
let relative_ordering = compare_mean_time(result, reference);

if result.mean == 0.0 {
return BenchmarkResultWithRelativeSpeed {
result,
relative_speed: if is_fastest { 1.0 } else { f64::INFINITY },
relative_speed: if is_reference { 1.0 } else { f64::INFINITY },
relative_speed_stddev: None,
is_fastest,
is_reference,
relative_ordering,
};
}

let ratio = result.mean / fastest.mean;
let ratio = match relative_ordering {
Ordering::Less => reference.mean / result.mean,
Ordering::Equal => 1.0,
Ordering::Greater => result.mean / reference.mean,
};

// https://en.wikipedia.org/wiki/Propagation_of_uncertainty#Example_formulas
// Covariance asssumed to be 0, i.e. variables are assumed to be independent
let ratio_stddev = match (result.stddev, fastest.stddev) {
let ratio_stddev = match (result.stddev, reference.stddev) {
(Some(result_stddev), Some(fastest_stddev)) => Some(
ratio
* ((result_stddev / result.mean).powi(2)
+ (fastest_stddev / fastest.mean).powi(2))
+ (fastest_stddev / reference.mean).powi(2))
.sqrt(),
),
_ => None,
Expand All @@ -59,7 +67,8 @@ fn compute_relative_speeds<'a>(
result,
relative_speed: ratio,
relative_speed_stddev: ratio_stddev,
is_fastest,
is_reference,
relative_ordering,
}
})
.collect();
Expand All @@ -74,6 +83,18 @@ fn compute_relative_speeds<'a>(
results
}

pub fn compute_with_check_from_reference<'a>(
results: &'a [BenchmarkResult],
reference: &'a BenchmarkResult,
sort_order: SortOrder,
) -> Option<Vec<BenchmarkResultWithRelativeSpeed<'a>>> {
if fastest_of(results).mean == 0.0 || reference.mean == 0.0 {
return None;
}

Some(compute_relative_speeds(results, reference, sort_order))
}

pub fn compute_with_check(
results: &[BenchmarkResult],
sort_order: SortOrder,
Expand Down Expand Up @@ -134,6 +155,20 @@ fn test_compute_relative_speed() {
assert_relative_eq!(2.5, annotated_results[2].relative_speed);
}

#[test]
fn test_compute_relative_speed_with_reference() {
use approx::assert_relative_eq;

let results = vec![create_result("cmd2", 2.0), create_result("cmd3", 5.0)];
let reference = create_result("cmd2", 4.0);

let annotated_results =
compute_with_check_from_reference(&results, &reference, SortOrder::Command).unwrap();

assert_relative_eq!(2.0, annotated_results[0].relative_speed);
assert_relative_eq!(1.25, annotated_results[1].relative_speed);
}

#[test]
fn test_compute_relative_speed_for_zero_times() {
let results = vec![create_result("cmd1", 1.0), create_result("cmd2", 0.0)];
Expand Down
63 changes: 47 additions & 16 deletions src/benchmark/scheduler.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
use colored::*;

use super::benchmark_result::BenchmarkResult;
use super::executor::{Executor, MockExecutor, RawExecutor, ShellExecutor};
use super::{relative_speed, Benchmark};
use colored::*;
use std::cmp::Ordering;

use crate::command::Commands;
use crate::command::{Command, Commands};
use crate::export::ExportManager;
use crate::options::{ExecutorKind, Options, OutputStyleOption, SortOrder};

Expand Down Expand Up @@ -38,9 +38,15 @@ impl<'a> Scheduler<'a> {
ExecutorKind::Shell(ref shell) => Box::new(ShellExecutor::new(shell, self.options)),
};

let reference = self
.options
.reference_command
.as_ref()
.map(|cmd| Command::new(None, cmd));

executor.calibrate()?;

for (number, cmd) in self.commands.iter().enumerate() {
for (number, cmd) in reference.iter().chain(self.commands.iter()).enumerate() {
self.results
.push(Benchmark::new(number, cmd, self.options, &*executor).run()?);

Expand All @@ -65,31 +71,56 @@ impl<'a> Scheduler<'a> {
return;
}

if let Some(annotated_results) = relative_speed::compute_with_check(
let reference = self
.options
.reference_command
.as_ref()
.map(|_| &self.results[0])
.unwrap_or_else(|| relative_speed::fastest_of(&self.results));

if let Some(annotated_results) = relative_speed::compute_with_check_from_reference(
&self.results,
reference,
self.options.sort_order_speed_comparison,
) {
match self.options.sort_order_speed_comparison {
SortOrder::MeanTime => {
println!("{}", "Summary".bold());

let fastest = annotated_results.iter().find(|r| r.is_fastest).unwrap();
let others = annotated_results.iter().filter(|r| !r.is_fastest);
let reference = annotated_results.iter().find(|r| r.is_reference).unwrap();
let others = annotated_results.iter().filter(|r| !r.is_reference);

println!(
" {} ran",
fastest.result.command_with_unused_parameters.cyan()
reference.result.command_with_unused_parameters.cyan()
);

for item in others {
let stddev = if let Some(stddev) = item.relative_speed_stddev {
format!(" ± {}", format!("{:.2}", stddev).green())
} else {
"".into()
};
let comparator = match item.relative_ordering {
Ordering::Less => format!(
"{}{} times slower than",
format!("{:8.2}", item.relative_speed).bold().green(),
stddev
),
Ordering::Greater => format!(
"{}{} times faster than",
format!("{:8.2}", item.relative_speed).bold().green(),
stddev
),
Ordering::Equal => format!(
" As fast ({}{}) as",
format!("{:.2}", item.relative_speed).bold().green(),
stddev
),
};
println!(
"{}{} times faster than {}",
format!("{:8.2}", item.relative_speed).bold().green(),
if let Some(stddev) = item.relative_speed_stddev {
format!(" ± {}", format!("{stddev:.2}").green())
} else {
"".into()
},
"{} {}",
comparator,
&item.result.command_with_unused_parameters.magenta()
);
}
Expand All @@ -101,7 +132,7 @@ impl<'a> Scheduler<'a> {
println!(
" {}{} {}",
format!("{:10.2}", item.relative_speed).bold().green(),
if item.is_fastest {
if item.is_reference {
" ".into()
} else if let Some(stddev) = item.relative_speed_stddev {
format!(" ± {}", format!("{stddev:5.2}").green())
Expand Down
10 changes: 10 additions & 0 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,16 @@ fn build_command() -> Command {
not every time as would happen with the --prepare option."
),
)
.arg(
Arg::new("reference")
.long("reference")
.action(ArgAction::Set)
.value_name("CMD")
.help(
"The reference command for the relative comparison of results. \
If this is unset, results are compared with the fastest command as reference."
)
)
.arg(
Arg::new("prepare")
.long("prepare")
Expand Down
2 changes: 1 addition & 1 deletion src/export/markup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ pub trait MarkupExporter {
let min_str = format_duration_value(measurement.min, Some(unit)).0;
let max_str = format_duration_value(measurement.max, Some(unit)).0;
let rel_str = format!("{:.2}", entry.relative_speed);
let rel_stddev_str = if entry.is_fastest {
let rel_stddev_str = if entry.is_reference {
"".into()
} else if let Some(stddev) = entry.relative_speed_stddev {
format!(" ± {stddev:.2}")
Expand Down
22 changes: 16 additions & 6 deletions src/options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,9 @@ pub struct Options {
/// Whether or not to ignore non-zero exit codes
pub command_failure_action: CmdFailureAction,

// Command to use as a reference for relative speed comparison
pub reference_command: Option<String>,

/// Command(s) to run before each timing run
pub preparation_command: Option<Vec<String>>,

Expand Down Expand Up @@ -245,6 +248,7 @@ impl Default for Options {
warmup_count: 0,
min_benchmarking_time: 3.0,
command_failure_action: CmdFailureAction::RaiseError,
reference_command: None,
preparation_command: None,
conclusion_command: None,
setup_command: None,
Expand Down Expand Up @@ -304,6 +308,8 @@ impl Options {

options.setup_command = matches.get_one::<String>("setup").map(String::from);

options.reference_command = matches.get_one::<String>("reference").map(String::from);

options.preparation_command = matches
.get_many::<String>("prepare")
.map(|values| values.map(String::from).collect::<Vec<String>>());
Expand Down Expand Up @@ -431,21 +437,25 @@ impl Options {
}

pub fn validate_against_command_list(&self, commands: &Commands) -> Result<()> {
let num_commands = commands.num_commands()
+ if self.reference_command.is_some() {
1
} else {
0
};
if let Some(preparation_command) = &self.preparation_command {
ensure!(
preparation_command.len() <= 1
|| commands.num_commands() == preparation_command.len(),
preparation_command.len() <= 1 || num_commands == preparation_command.len(),
"The '--prepare' option has to be provided just once or N times, where N is the \
number of benchmark commands."
number of benchmark commands including a potential reference."
);
}

if let Some(conclusion_command) = &self.conclusion_command {
ensure!(
conclusion_command.len() <= 1
|| commands.num_commands() == conclusion_command.len(),
conclusion_command.len() <= 1 || num_commands == conclusion_command.len(),
"The '--conclude' option has to be provided just once or N times, where N is the \
number of benchmark commands."
number of benchmark commands including a potential reference."
);
}

Expand Down
Loading