Benchmark improvements
* CI: Clean offline; display cleaned bytes; one artifact upload * Add missing csv bench to iai benchmarks * Generate flamegraphs for benchmarks * Validate benchmark results separately
This commit is contained in:
@@ -4,16 +4,16 @@ The `benches` directory contains benchmarks that test scryer-prolog performance.
|
||||
|
||||
Benchmarks are run via two harnesses:
|
||||
|
||||
* `criterion` - criterion performs statistical analysis of benchmark runs and is
|
||||
great for benchmarking locally.
|
||||
* `iai-callgrind` - this runs the benchmark with callgrind, which is able to
|
||||
precisely track the number of instructions executed during the run. This is
|
||||
especially helpful in a public CI runner context where neighboring VMs can
|
||||
cause a very high wall time variance. This means that it doesn't track wall
|
||||
time which is what we really care about, but it is a good tradeoff for CI
|
||||
where tracking runtime is unreliable.
|
||||
* `criterion` - criterion performs statistical analysis of benchmark runs and is
|
||||
great for benchmarking locally.
|
||||
cause a very high wall time variance. While instructions executed is only
|
||||
correlated with the desired metric (wall time), this is a good tradeoff for CI
|
||||
where that metric is unreliable.
|
||||
|
||||
Run them using the following commands:
|
||||
Run benchmarks with the following commands:
|
||||
|
||||
```
|
||||
cargo bench --bench run_criterion
|
||||
@@ -21,6 +21,9 @@ cargo bench --bench run_criterion
|
||||
# run a particular criterion benchmark
|
||||
cargo bench --bench run_criterion -- <benchmark_name>
|
||||
|
||||
# run in profiling mode which outputs flamegraphs. Set profile time in seconds:
|
||||
cargo bench --bench run_criterion -- --profile-time <time>
|
||||
|
||||
# to run iai, you need valgrind installed and to install iai-callgrind-runner
|
||||
# at the same version as is in Cargo.toml:
|
||||
cargo install iai-callgrind-runner --version 0.7.3
|
||||
@@ -29,7 +32,7 @@ cargo bench --bench run_iai
|
||||
```
|
||||
|
||||
For consistency, both runners -- `run_iai.rs` and `run_criterion.rs` -- import
|
||||
the same setup code from `benches.rs`.
|
||||
the same setup code from `setup.rs`.
|
||||
|
||||
## Setup
|
||||
|
||||
@@ -50,45 +53,43 @@ This design is meant to suppoort defining lots of benchmarks.
|
||||
|
||||
To add a new benchmark:
|
||||
|
||||
* Add a new file `benches/[module].pl` that contains setup code. Import
|
||||
* Add a new file `benches/[module].pl` that contains setup prolog code. Import
|
||||
libraries, define predicates, etc.
|
||||
* Add a new section in `setup.rs::benches()` that refers to it and add some
|
||||
benchmarks.
|
||||
* Add a new section in `setup.rs::prolog_benchmarks()` that refers to to the
|
||||
file and write a query to be benchmarked.
|
||||
* If the query mutates the machine, then use `Strategy::Fresh` so the criterion
|
||||
benchmark will recreate a new machine for each benchmark run, otherwise use
|
||||
`Strategy::Reuse` which has lower overhead. (This is not used by the iai
|
||||
benchmark because it only runs once anyway.)
|
||||
|
||||
Some tips:
|
||||
|
||||
* The goal of benchmarking is to know if a library or engine change improved
|
||||
performance or not.
|
||||
* Once a benchmark is defined and named, avoid changing it's definition. In
|
||||
general, if a benchmark needs to change to be more useful, give the new
|
||||
definition a new name. This will prevent charts from showing wild changes in
|
||||
* Once a benchmark is defined and named, avoid changing it's definition. If a
|
||||
benchmark needs to change to be more useful, give the new definition a new
|
||||
name instead. This will prevent charts from showing wild changes in
|
||||
performance just because the definition changed (see previous).
|
||||
* Aim for queries to execute in less than 0.5s realtime. Longer runtimes make it
|
||||
easier for humans to see big differences, but benchmarks either run 10x slower
|
||||
(iai) or execute repeatedly to attain statistical significance (criterion) and
|
||||
in both cases queries that take longer become cumbersome to run.
|
||||
in both cases benchmarking queries that take longer than about 0.5s are
|
||||
cumbersome to run.
|
||||
* Consider that the library runtime actually parses the text output of the top
|
||||
level. So don't use custom outputs or it will fail to parse. Also keep the
|
||||
output small so it doesn't just benchmark the ouput parsing code.
|
||||
* DO test the output of the benchmark run, we don't want to count broken
|
||||
benchmarks.
|
||||
* Because a query may run against the same machine multiple times, don't
|
||||
permanently mutate the state of the engine with the query since that will
|
||||
taint subsequent runs. (Benchmarking assertz et al is desirable, but will
|
||||
require some adjustments to how the machine is set up for runs.)
|
||||
|
||||
## CI
|
||||
|
||||
Both benchmark harnesses are run in `.github/workflows/ci.yaml` in the `report`
|
||||
job, and the results are published as build artifacts.
|
||||
|
||||
A future action may consume the build artifacts and publish a report using the
|
||||
results.
|
||||
|
||||
## Todo
|
||||
|
||||
- [ ] Currently, the execution time to load a module is not benchmarked. It
|
||||
would be nice to have at least one benchmark for loading a module (probably a
|
||||
big one).
|
||||
- [ ] Write a new action that consumes the test and benchmark results and plots
|
||||
them over time and publishes a report (github pages?).
|
||||
- [ ] Write a new action that downloads the test and benchmark results
|
||||
artifacts, plots them over time, and publishes a report to github pages.
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
use criterion::{criterion_group, criterion_main, BatchSize, Criterion};
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
use pprof::criterion::{Output, PProfProfiler};
|
||||
|
||||
mod setup;
|
||||
|
||||
fn bench_criterion(c: &mut Criterion) {
|
||||
@@ -13,9 +16,21 @@ fn bench_criterion(c: &mut Criterion) {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
fn config() -> Criterion {
|
||||
Criterion::default()
|
||||
.sample_size(20)
|
||||
.with_profiler(PProfProfiler::new(100, Output::Flamegraph(None)))
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn config() -> Criterion {
|
||||
Criterion::default().sample_size(20)
|
||||
}
|
||||
|
||||
criterion_group!(
|
||||
name = bench_group;
|
||||
config = Criterion::default().sample_size(10);
|
||||
name = benches;
|
||||
config = config();
|
||||
targets = bench_criterion
|
||||
);
|
||||
criterion_main!(bench_group);
|
||||
criterion_main!(benches);
|
||||
|
||||
@@ -1,21 +1,18 @@
|
||||
use iai_callgrind::{library_benchmark, library_benchmark_group, main};
|
||||
use scryer_prolog::machine::parsed_results::QueryResolution;
|
||||
|
||||
mod setup;
|
||||
|
||||
#[library_benchmark]
|
||||
#[bench::normal(setup::prolog_benches()["count_edges_short"].setup())]
|
||||
fn bench_edges(mut run: impl FnMut()) {
|
||||
run();
|
||||
}
|
||||
|
||||
#[library_benchmark]
|
||||
#[bench::normal(setup::prolog_benches()["numlist_short"].setup())]
|
||||
fn bench_numlist(mut run: impl FnMut()) {
|
||||
run();
|
||||
#[bench::count_edges(setup::prolog_benches()["count_edges"].setup())]
|
||||
#[bench::numlist(setup::prolog_benches()["numlist"].setup())]
|
||||
#[bench::csv_codename(setup::prolog_benches()["csv_codename"].setup())]
|
||||
fn bench(mut run: impl FnMut() -> QueryResolution) -> QueryResolution {
|
||||
run()
|
||||
}
|
||||
|
||||
library_benchmark_group!(
|
||||
name = bench_group;
|
||||
benchmarks = bench_edges, bench_numlist
|
||||
name = benches;
|
||||
benchmarks = bench
|
||||
);
|
||||
main!(library_benchmark_groups = bench_group);
|
||||
main!(library_benchmark_groups = benches);
|
||||
|
||||
@@ -2,7 +2,7 @@ use std::{collections::BTreeMap, fs, path::Path};
|
||||
|
||||
use maplit::btreemap;
|
||||
use scryer_prolog::machine::{
|
||||
parsed_results::{QueryMatch, QueryResolution, Value},
|
||||
parsed_results::{QueryResolution, Value},
|
||||
Machine,
|
||||
};
|
||||
|
||||
@@ -10,20 +10,13 @@ pub fn prolog_benches() -> BTreeMap<&'static str, PrologBenchmark> {
|
||||
[
|
||||
(
|
||||
"count_edges", // name of the benchmark
|
||||
"benches/edges.pl", // name of the prolog module file to load
|
||||
"independent_set_count(aa, Count).", // query to benchmark in the context of the loaded module
|
||||
Strategy::Reuse,
|
||||
btreemap! { "Count" => Value::try_from("211954906".to_string()).unwrap(), }, // list of expected bindings
|
||||
),
|
||||
(
|
||||
"count_edges_short",
|
||||
"benches/edges.pl", // use the same file in multiple benchmarks
|
||||
"independent_set_count(ky, Count).", // consider making the query adjustable to tune the run time to ~0.1s
|
||||
"benches/edges.pl", // name of the prolog module file to load. use the same file in multiple benchmarks
|
||||
"independent_set_count(ky, Count).", // query to benchmark in the context of the loaded module. consider making the query adjustable to tune the run time to ~0.1s
|
||||
Strategy::Reuse,
|
||||
btreemap! { "Count" => Value::try_from("2869176".to_string()).unwrap() },
|
||||
),
|
||||
(
|
||||
"numlist_short",
|
||||
"numlist",
|
||||
"benches/numlist.pl",
|
||||
"run_numlist(1000000, Head).",
|
||||
Strategy::Reuse,
|
||||
@@ -67,27 +60,40 @@ pub struct PrologBenchmark {
|
||||
}
|
||||
|
||||
impl PrologBenchmark {
|
||||
pub fn setup(&self) -> impl FnMut() {
|
||||
pub fn make_machine(&self) -> Machine {
|
||||
let program = fs::read_to_string(self.filename).unwrap();
|
||||
let module_name = Path::new(self.filename)
|
||||
.file_stem()
|
||||
.and_then(|s| s.to_str())
|
||||
.unwrap();
|
||||
|
||||
let mut machine = Machine::new_lib();
|
||||
machine.load_module_string(module_name, program);
|
||||
machine
|
||||
}
|
||||
|
||||
let benchmark_name = self.name;
|
||||
pub fn setup(&self) -> impl FnMut() -> QueryResolution {
|
||||
let mut machine = self.make_machine();
|
||||
let query = self.query;
|
||||
let expected = QueryResolution::Matches(vec![QueryMatch::from(self.bindings.clone())]);
|
||||
|
||||
move || {
|
||||
use criterion::black_box;
|
||||
let result = black_box(machine.run_query(black_box(query.to_string())));
|
||||
match result {
|
||||
Ok(r) => assert_eq!(&r, &expected),
|
||||
Err(e) => panic!("benchmark {} failed with: {}", benchmark_name, e),
|
||||
}
|
||||
black_box(machine.run_query(black_box(query.to_string()))).unwrap()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
#[test]
|
||||
fn validate_benchmarks() {
|
||||
use super::prolog_benches;
|
||||
use scryer_prolog::machine::parsed_results::QueryResolution;
|
||||
|
||||
use scryer_prolog::machine::parsed_results::QueryMatch;
|
||||
for (_, r) in prolog_benches() {
|
||||
let mut machine = r.make_machine();
|
||||
let result = machine.run_query(r.query.to_string()).unwrap();
|
||||
let expected = QueryResolution::Matches(vec![QueryMatch::from(r.bindings.clone())]);
|
||||
assert_eq!(result, expected, "validating benchmark {}", r.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user