Skip to content

Commit dde8af1

Browse files
committed
Add microbenchmarks
1 parent bfd05ee commit dde8af1

17 files changed

+283
-0
lines changed

benches/README.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,33 @@ in two ways:
2222
1. The time to parse the file to AST
2323
2. The time it takes to execute the file
2424

25+
### Adding a micro benchmark
26+
27+
Micro benchmarks are small snippets of code added under the `microbenchmarks/` directory. A microbenchmark file has
28+
two sections:
29+
1. Optional setup code
30+
2. The code to be benchmarked
31+
32+
These two sections are delimited by `# ---`. For example:
33+
34+
```python
35+
a_list = [1,2,3]
36+
37+
# ---
38+
39+
len(a_list)
40+
```
41+
42+
Only `len(a_list)` will be timed. Setup or benchmarked code can optionally reference a variable called `ITERATIONS`. If
43+
present then the benchmark code will be invoked 5 times with `ITERATIONS` set to a value between 100 and 1,000. For
44+
example:
45+
46+
```python
47+
obj = [i for i in range(ITERATIONS)]
48+
```
49+
50+
`ITERATIONS` can appear in both the setup code and the benchmark code.
51+
2552
## MacOS setup
2653

2754
On MacOS you will need to add the following to a `.cargo/config` file:

benches/microbenchmarks.rs

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
use cpython::Python;
2+
use criterion::measurement::WallTime;
3+
use criterion::{
4+
criterion_group, criterion_main, BatchSize, BenchmarkGroup, BenchmarkId, Criterion, Throughput,
5+
};
6+
use rustpython_compiler::Mode;
7+
use rustpython_vm::pyobject::ItemProtocol;
8+
use rustpython_vm::pyobject::PyResult;
9+
use rustpython_vm::Interpreter;
10+
use std::path::{Path, PathBuf};
11+
use std::{fs, io};
12+
13+
pub struct MicroBenchmark {
14+
name: String,
15+
setup: String,
16+
code: String,
17+
iterate: bool,
18+
}
19+
//
20+
// fn bench_cpython_code(b: &mut Bencher, source: &str) {
21+
// let gil = cpython::Python::acquire_gil();
22+
// let python = gil.python();
23+
//
24+
// b.iter(|| {
25+
// let res: cpython::PyResult<()> = python.run(source, None, None);
26+
// if let Err(e) = res {
27+
// e.print(python);
28+
// panic!("Error running source")
29+
// }
30+
// });
31+
// }
32+
33+
fn bench_cpython_code(group: &mut BenchmarkGroup<WallTime>, bench: &MicroBenchmark) {
34+
let gil = cpython::Python::acquire_gil();
35+
let python = gil.python();
36+
37+
let bench_func = |(python, code): (Python, String)| {
38+
let res: cpython::PyResult<()> = python.run(&code, None, None);
39+
if let Err(e) = res {
40+
e.print(python);
41+
panic!("Error running microbenchmark")
42+
}
43+
};
44+
45+
let bench_setup = |iterations| {
46+
let code = if let Some(idx) = iterations {
47+
// We can't easily modify the locals when running cPython. So we just add the
48+
// loop iterations at the top of the code...
49+
format!("ITERATIONS = {}\n{}", idx, bench.code)
50+
} else {
51+
(&bench.code).to_string()
52+
};
53+
54+
let res: cpython::PyResult<()> = python.run(&bench.setup, None, None);
55+
if let Err(e) = res {
56+
e.print(python);
57+
panic!("Error running microbenchmark setup code")
58+
}
59+
(python, code)
60+
};
61+
62+
if bench.iterate {
63+
for idx in (100..=1_000).step_by(200) {
64+
group.throughput(Throughput::Elements(idx as u64));
65+
group.bench_with_input(BenchmarkId::new("cpython", &bench.name), &idx, |b, idx| {
66+
b.iter_batched(
67+
|| bench_setup(Some(*idx)),
68+
bench_func,
69+
BatchSize::PerIteration,
70+
);
71+
});
72+
}
73+
} else {
74+
group.bench_function(BenchmarkId::new("cpython", &bench.name), move |b| {
75+
b.iter_batched(|| bench_setup(None), bench_func, BatchSize::PerIteration);
76+
});
77+
}
78+
}
79+
80+
fn bench_rustpy_code(group: &mut BenchmarkGroup<WallTime>, bench: &MicroBenchmark) {
81+
Interpreter::default().enter(|vm| {
82+
let setup_code = vm
83+
.compile(&bench.setup, Mode::Exec, bench.name.to_owned())
84+
.expect("Error compiling setup code");
85+
let bench_code = vm
86+
.compile(&bench.code, Mode::Exec, bench.name.to_owned())
87+
.expect("Error compiling bench code");
88+
89+
let bench_func = |(scope, bench_code)| {
90+
let res: PyResult = vm.run_code_obj(bench_code, scope);
91+
vm.unwrap_pyresult(res);
92+
};
93+
94+
let bench_setup = |iterations| {
95+
let scope = vm.new_scope_with_builtins();
96+
if let Some(idx) = iterations {
97+
scope
98+
.locals
99+
.set_item(vm.ctx.new_str("ITERATIONS"), vm.ctx.new_int(idx), vm)
100+
.expect("Error adding ITERATIONS local variable");
101+
}
102+
vm.run_code_obj(setup_code.clone(), scope.clone())
103+
.expect("Error running benchmark setup code");
104+
(scope, bench_code.clone())
105+
};
106+
107+
if bench.iterate {
108+
for idx in (100..=1_000).step_by(200) {
109+
group.throughput(Throughput::Elements(idx as u64));
110+
group.bench_with_input(
111+
BenchmarkId::new("rustpython", &bench.name),
112+
&idx,
113+
|b, idx| {
114+
b.iter_batched(
115+
|| bench_setup(Some(*idx)),
116+
bench_func,
117+
BatchSize::PerIteration,
118+
);
119+
},
120+
);
121+
}
122+
} else {
123+
group.bench_function(BenchmarkId::new("rustpython", &bench.name), move |b| {
124+
b.iter_batched(|| bench_setup(None), bench_func, BatchSize::PerIteration);
125+
});
126+
}
127+
})
128+
}
129+
130+
pub fn run_micro_benchmark(c: &mut Criterion, benchmark: MicroBenchmark) {
131+
let mut group = c.benchmark_group("microbenchmarks");
132+
133+
bench_cpython_code(&mut group, &benchmark);
134+
bench_rustpy_code(&mut group, &benchmark);
135+
136+
group.finish();
137+
}
138+
139+
pub fn criterion_benchmark(c: &mut Criterion) {
140+
let benchmark_dir = Path::new("./benches/microbenchmarks/");
141+
let dirs: Vec<fs::DirEntry> = benchmark_dir
142+
.read_dir()
143+
.unwrap()
144+
.collect::<io::Result<_>>()
145+
.unwrap();
146+
let paths: Vec<PathBuf> = dirs.iter().map(|p| p.path()).collect();
147+
148+
let benchmarks: Vec<MicroBenchmark> = paths
149+
.into_iter()
150+
.map(|p| {
151+
let name = p.file_name().unwrap().to_os_string();
152+
let contents = fs::read_to_string(p).unwrap();
153+
let iterate = contents.contains("ITERATIONS");
154+
155+
let (setup, code) = if contents.contains("# ---") {
156+
let split: Vec<&str> = contents.splitn(2, "# ---").collect();
157+
(split[0].to_string(), split[1].to_string())
158+
} else {
159+
("".to_string(), contents)
160+
};
161+
let name = name.into_string().unwrap();
162+
MicroBenchmark {
163+
name,
164+
setup,
165+
code,
166+
iterate,
167+
}
168+
})
169+
.collect();
170+
171+
for benchmark in benchmarks {
172+
run_micro_benchmark(c, benchmark);
173+
}
174+
}
175+
176+
criterion_group!(benches, criterion_benchmark);
177+
criterion_main!(benches);

benches/microbenchmarks/addition.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
total = 0
2+
for i in range(ITERATIONS):
3+
total += i
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
def add(a, b):
2+
a + b
3+
4+
5+
# ---
6+
7+
add(a=1, b=10)
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
def add(a, b):
2+
a + b
3+
4+
5+
# ---
6+
7+
add(1, 2)
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
class Foo:
2+
ABC = 1
3+
4+
def __init__(self):
5+
super().__init__()
6+
7+
def bar(self):
8+
pass
9+
10+
@classmethod
11+
def bar_2(cls):
12+
pass
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
obj = {i: i for i in range(ITERATIONS)}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
obj = [i for i in range(ITERATIONS)]
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
obj = {i for i in range(ITERATIONS)}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
class Foo:
2+
pass
3+
4+
5+
# ---
6+
7+
Foo()
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
class Foo:
2+
pass
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
def function():
2+
pass
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
from contextlib import contextmanager
2+
3+
@contextmanager
4+
def try_catch(*args, **kwargs):
5+
try:
6+
yield
7+
except RuntimeError:
8+
pass
9+
10+
# ---
11+
12+
with try_catch():
13+
raise RuntimeError()
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
try:
2+
try:
3+
raise ValueError()
4+
except ValueError as e:
5+
raise RuntimeError() from e
6+
except RuntimeError as e:
7+
pass
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
try:
2+
raise RuntimeError()
3+
except RuntimeError as e:
4+
pass
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
obj = []
2+
3+
# ---
4+
5+
for i in range(ITERATIONS):
6+
obj.append(i)
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
string = "a" * ITERATIONS
2+
3+
# ---
4+
5+
for char in string:
6+
pass

0 commit comments

Comments
 (0)