So You Want to Build a Language VM - Part 20 - Benchmarks
Adds in Criterion to do benchmarks
Doh
We’ve been having so much fun, we haven’t written any benchmarks! Though it isn’t the most exciting thing to write, they are important. == Benchmarks There’s two things to understand about benchmarks:
A single benchmark with no context is not helpful
They are most useful when tracked over time
Any thing you can benchmark, you can tune to be good at that specific benchmark.
Useful Benchmarks
I find benchmarks most helpful when:
You collect them from many sub-components of a large system or application
You keep a historical record, so you can see trends
If we are building a system that consists of multiple sub-components like:
Web frontend
Backend
Database
Load Balancers
Useful benchmarks for this would be things like:
How many concurrent connections can the load balancers sustain on server type X? I don’t really care how fast it can factor prime numbers.
How effective is the caching layer of the database server? I don’t care that its RAM timings are 3ns faster.
Anyway, enough rambling. Suffice to say, we should benchmark small components in isolation and track their performance over time.
Adding in Criterion
Rust has a benchmarking system, but it isn’t available on stable. The go-to crate for benchmarking is Criterion, so let’s get going. =)
Cargo
Add the following to your Cargo.toml:
[[bench]]
name = "iridium"
harness = false
Benches
Next up, make a new directory at benches/
. That’s right, same level as src/
. This file is where we will put our benchmark functions.
In benches/
, create iridium.rs
with the following contents:
#[macro_use]
extern crate criterion;
extern crate iridium;
use criterion::Criterion;
use iridium::vm::VM;
use iridium::assembler::{PIE_HEADER_PREFIX, PIE_HEADER_LENGTH};
Let’s start with a simple benchmark of the VM executing arithmetic instructions. Add this next in the iridium.rs file:
mod arithmetic {
use super::*;
fn execute_add(c: &mut Criterion) {
let clos = {
let mut test_vm = get_test_vm();
test_vm.program = vec![1, 0, 1, 2];
test_vm.run_once();
};
c.bench_function(
"execute_add",
move |b| b.iter(|| clos
)
);
}
}
As you can see, it’s almost identical to our test for that:
#[test]
fn test_add_opcode() {
let mut test_vm = get_test_vm();
test_vm.program = vec![1, 0, 1, 2];
test_vm.program = prepend_header(test_vm.program);
test_vm.run();
assert_eq!(test_vm.registers[2], 15);
}
I did not know that we could bind a series of expressions to a variable! That’s so cool. All we do is replicate the tests for each of the 4 arithmetic instructions and add them in.
Missing Functions
You may notice we don’t have the get_test_vm
function, nor do we have the prepend_header
function. Since those have become useful outside of the tests in vm.rs
, let’s move them up to public functions.
Head over to src/vm.rs
. We have a bit of re-arranging to do.
First, let’s move the prepend_header
and get_test_vm
functions up to the VM
trait and make them public. This way, we can call them like: VM::prepend_header(…)
. This will break a few things:
Change
use assembler::PIE_HEADER_PREFIX;
touse assembler::{PIE_HEADER_PREFIX, PIE_HEADER_LENGTH};
In every test that uses
prepend_header
orget_test_vm
, preface the function call withVM::
. I did this with a search and replace, but you can use the code in the repo if you prefer. =)
Now back in benches/iridium.rs
, we can do:
mod arithmetic {
use super::*;
fn execute_add(c: &mut Criterion) {
let clos = {
let mut test_vm = VM::get_test_vm();
test_vm.program = vec![1, 0, 1, 2];
test_vm.run_once();
};
c.bench_function(
"execute_add",
move |b| b.iter(|| clos
)
);
}
}
Running the Benches
Almost there! Now we need to use the criterion macros to setup our benchmark groups. At the end of the arithmetic module, put:
criterion_group!{
name = arithmetic;
config = Criterion::default();
targets = execute_add, execute_sub, execute_mul, execute_div,
}
Yes, that is supposed to be {
and }
, not (
and )
in the macro. As the final line in the iridium.rs
put:
criterion_main!(arithmetic::arithmetic);
And that’s it! Add benchmark functions (or look in the repo) for each of the arithmetic operators. From the root of the iridium/
directory, we can now run cargo bench
and we should see this:
<snip a lot of stuff before this we don't care about>
Running target/release/deps/iridium-b5264c6303e130cb
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
Running target/release/deps/iridium-59eea43f05a081ed
execute_add time: [0.0000 ps 0.0000 ps 0.0000 ps]
change: [-35.123% +1503.3% +5337.3%] (p = 0.39 > 0.05)
No change in performance detected.
Found 13 outliers among 100 measurements (13.00%)
4 (4.00%) high mild
9 (9.00%) high severe
execute_sub time: [0.0000 ps 0.0000 ps 0.0000 ps]
change: [-54.712% -12.150% +78.688%] (p = 0.73 > 0.05)
No change in performance detected.
Found 13 outliers among 100 measurements (13.00%)
5 (5.00%) high mild
8 (8.00%) high severe
execute_mul time: [0.0000 ps 0.0000 ps 0.0000 ps]
change: [-50.926% -1.5089% +101.21%] (p = 0.97 > 0.05)
No change in performance detected.
Found 12 outliers among 100 measurements (12.00%)
4 (4.00%) high mild
8 (8.00%) high severe
execute_div time: [0.0000 ps 0.0000 ps 0.0000 ps]
change: [-48.559% -5.5472% +73.134%] (p = 0.87 > 0.05)
No change in performance detected.
Found 11 outliers among 100 measurements (11.00%)
3 (3.00%) high mild
8 (8.00%) high severe
If you look in targets/criterion/
you will see nice graphs that criterion output.
Keeping the Benchmark Graphs
If we want to keep these graphs so we can compare runs over time, we need to put them somewhere. Since we use Appveyor, Gitlab, and Travis to build Iridium for multiple platforms, and each of those platforms will have benchmarks run, we need to keep them. The easiest thing to do is to specify that they are artifacts and keep them from each build, along with the binary, for each platform.
I won’t go through all that here, but you can check out the .travis.yml
, .gitlab-ci.yml
and appveyor.yml
to see how I did it.
End
I think that is a good stopping point. There’s TONS more benchmarks we need to write, and we’ll add more in as we go.
See you next time!
If you need some assistance with any of the topics in the tutorials, or just devops and application development in general, we offer consulting services. Check it out over here or click Services along the top.