Skip to content

Commit e2b7ad7

Browse files
committed
bench: a better V GC bench
1 parent 0c62500 commit e2b7ad7

1 file changed

Lines changed: 251 additions & 32 deletions

File tree

‎bench/bench_gc.v‎

Lines changed: 251 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,22 @@
1-
// bench_gc.v - Benchmark comparing VGC vs Boehm GC
2-
// Run with: v -gc boehm -o bench_boehm bench/bench_gc.v && ./bench_boehm
3-
// v -gc vgc -o bench_vgc bench/bench_gc.v && ./bench_vgc
1+
// bench_gc.v - Benchmark comparing Boehm GC vs V GC (vgc)
2+
//
3+
// Usage (runner mode - compiles and compares both):
4+
// v run bench/bench_gc.v
5+
//
6+
// Usage (individual, to test a specific GC mode):
7+
// v -gc boehm -prod -o /tmp/bench_boehm bench/bench_gc.v && /tmp/bench_boehm
8+
// v -gc vgc -prod -o /tmp/bench_vgc bench/bench_gc.v && /tmp/bench_vgc
9+
module main
10+
11+
import os
412
import time
513

614
const n_allocs = 1_000_000
715
const n_iters = 5
16+
const tree_depth = 18
17+
const tree_rounds = 10
18+
const array_rounds = 100
19+
const array_size = 100_000
820

921
struct Node {
1022
mut:
@@ -13,16 +25,27 @@ mut:
1325
value int
1426
}
1527

28+
fn gc_mode_name() string {
29+
return $if gcboehm ? {
30+
'boehm'
31+
} $else $if vgc ? {
32+
'vgc'
33+
} $else {
34+
'none'
35+
}
36+
}
37+
38+
// --- Workloads ---
39+
1640
// Allocate many small objects (typical GC workload)
17-
fn bench_small_allocs() {
41+
fn bench_small_allocs() (i64, int) {
1842
sw := time.new_stopwatch()
1943
mut sum := 0
2044
for _ in 0 .. n_allocs {
2145
s := 'hello world ${sum}'
2246
sum += s.len
2347
}
24-
elapsed := sw.elapsed()
25-
eprintln(' small allocs (${n_allocs}x string): ${elapsed.milliseconds()} ms (sum=${sum})')
48+
return sw.elapsed().milliseconds(), sum
2649
}
2750

2851
// Build a binary tree (tests pointer-heavy allocation)
@@ -46,51 +69,247 @@ fn tree_sum(n &Node) int {
4669
return n.value + tree_sum(n.left) + tree_sum(n.right)
4770
}
4871

49-
fn bench_tree() {
72+
fn bench_tree() (i64, int) {
5073
sw := time.new_stopwatch()
5174
mut total := 0
52-
for _ in 0 .. 10 {
53-
t := make_tree(18)
75+
for _ in 0 .. tree_rounds {
76+
t := make_tree(tree_depth)
5477
total += tree_sum(t)
5578
}
56-
elapsed := sw.elapsed()
57-
eprintln(' tree build+walk (depth=18, 10x): ${elapsed.milliseconds()} ms (sum=${total})')
79+
return sw.elapsed().milliseconds(), total
5880
}
5981

6082
// Allocate and discard arrays (tests realloc / growing)
61-
fn bench_arrays() {
83+
fn bench_arrays() (i64, int) {
6284
sw := time.new_stopwatch()
6385
mut total := 0
64-
for _ in 0 .. 100 {
86+
for _ in 0 .. array_rounds {
6587
mut arr := []int{cap: 16}
66-
for j in 0 .. 100_000 {
88+
for j in 0 .. array_size {
6789
arr << j
6890
}
6991
total += arr.len
7092
}
71-
elapsed := sw.elapsed()
72-
eprintln(' array grow (100x 100k pushes): ${elapsed.milliseconds()} ms (total=${total})')
93+
return sw.elapsed().milliseconds(), total
7394
}
7495

75-
fn main() {
76-
gc_mode := $if gcboehm ? {
77-
'boehm'
78-
} $else $if vgc ? {
79-
'vgc'
80-
} $else {
81-
'none'
96+
// Allocate many maps
97+
fn bench_maps() (i64, int) {
98+
sw := time.new_stopwatch()
99+
mut total := 0
100+
for round in 0 .. 20 {
101+
mut m := map[string]int{}
102+
for j in 0 .. 10_000 {
103+
m['key_${round}_${j}'] = j
104+
}
105+
total += m.len
82106
}
83-
eprintln('=== GC Benchmark (mode: ${gc_mode}) ===')
107+
return sw.elapsed().milliseconds(), total
108+
}
84109

85-
for iter in 0 .. n_iters {
86-
eprintln('--- iteration ${iter + 1}/${n_iters} ---')
87-
bench_small_allocs()
88-
bench_tree()
89-
bench_arrays()
110+
// Mixed workload: interleave allocations and collections
111+
fn bench_mixed() (i64, int) {
112+
sw := time.new_stopwatch()
113+
mut total := 0
114+
for _ in 0 .. 50 {
115+
// strings
116+
mut strs := []string{cap: 1000}
117+
for j in 0 .. 1000 {
118+
strs << 'item_${j}_${'x'.repeat(j % 50)}'
119+
}
120+
total += strs.len
121+
// tree
122+
t := make_tree(12)
123+
total += tree_sum(t)
124+
// array
125+
mut arr := []int{cap: 10_000}
126+
for j in 0 .. 10_000 {
127+
arr << j * 2
128+
}
129+
total += arr.len
90130
}
131+
return sw.elapsed().milliseconds(), total
132+
}
133+
134+
// --- Workload runner (compiled with a specific GC) ---
91135

136+
fn run_workload() {
137+
mode := gc_mode_name()
138+
for iter in 0 .. n_iters {
139+
ms_alloc, sum_alloc := bench_small_allocs()
140+
ms_tree, sum_tree := bench_tree()
141+
ms_array, sum_array := bench_arrays()
142+
ms_map, sum_map := bench_maps()
143+
ms_mixed, sum_mixed := bench_mixed()
144+
145+
// Machine-readable output: iter,test,ms,checksum
146+
println('${iter},allocs,${ms_alloc},${sum_alloc}')
147+
println('${iter},tree,${ms_tree},${sum_tree}')
148+
println('${iter},arrays,${ms_array},${sum_array}')
149+
println('${iter},maps,${ms_map},${sum_map}')
150+
println('${iter},mixed,${ms_mixed},${sum_mixed}')
151+
}
92152
usage := gc_heap_usage()
93-
eprintln('--- heap usage ---')
94-
eprintln(' heap_size: ${usage.heap_size / 1024} KB')
95-
eprintln(' free_bytes: ${usage.free_bytes / 1024} KB')
153+
println('heap,${usage.heap_size},${usage.free_bytes}')
154+
println('gc_mode,${mode}')
155+
}
156+
157+
// --- Runner mode: compile with both GCs and compare ---
158+
159+
struct BenchData {
160+
mut:
161+
times [5][]i64 // indexed by test (allocs=0, tree=1, arrays=2, maps=3, mixed=4)
162+
heap i64
163+
free i64
164+
mode string
165+
}
166+
167+
fn test_index(name string) int {
168+
return match name {
169+
'allocs' { 0 }
170+
'tree' { 1 }
171+
'arrays' { 2 }
172+
'maps' { 3 }
173+
'mixed' { 4 }
174+
else { -1 }
175+
}
176+
}
177+
178+
fn test_name(idx int) string {
179+
return ['small allocs (${n_allocs}x string)',
180+
'tree build+walk (depth=${tree_depth}, ${tree_rounds}x)',
181+
'array grow (${array_rounds}x ${array_size} pushes)', 'map insert (20x 10k entries)',
182+
'mixed workload (50 rounds)'][idx]
183+
}
184+
185+
fn median(mut vals []i64) i64 {
186+
if vals.len == 0 {
187+
return 0
188+
}
189+
vals.sort(a < b)
190+
return vals[vals.len / 2]
191+
}
192+
193+
fn parse_output(output string) BenchData {
194+
mut d := BenchData{}
195+
for line in output.split_into_lines() {
196+
parts := line.split(',')
197+
if parts.len < 2 {
198+
continue
199+
}
200+
if parts[0] == 'heap' && parts.len >= 3 {
201+
d.heap = parts[1].i64()
202+
d.free = parts[2].i64()
203+
} else if parts[0] == 'gc_mode' {
204+
d.mode = parts[1]
205+
} else if parts.len >= 4 {
206+
ti := test_index(parts[1])
207+
if ti >= 0 {
208+
d.times[ti] << parts[2].i64()
209+
}
210+
}
211+
}
212+
return d
213+
}
214+
215+
fn rpad(s string, width int) string {
216+
if s.len >= width {
217+
return s
218+
}
219+
return s + ' '.repeat(width - s.len)
220+
}
221+
222+
fn lpad(s string, width int) string {
223+
if s.len >= width {
224+
return s
225+
}
226+
return ' '.repeat(width - s.len) + s
227+
}
228+
229+
fn run_comparison() {
230+
v_exe := os.getenv_opt('VEXE') or { @VEXE }
231+
src := os.join_path(@VMODROOT, 'bench', 'bench_gc.v')
232+
tmp_boehm := os.join_path(os.temp_dir(), 'bench_gc_boehm')
233+
tmp_vgc := os.join_path(os.temp_dir(), 'bench_gc_vgc')
234+
235+
println('=== GC Benchmark: Boehm vs VGC ===')
236+
println('')
237+
238+
// Compile both
239+
for gc_mode in ['boehm', 'vgc'] {
240+
out := if gc_mode == 'boehm' { tmp_boehm } else { tmp_vgc }
241+
print('compiling with -gc ${gc_mode}...')
242+
flush_stdout()
243+
r := os.execute('${v_exe} -gc ${gc_mode} -prod -o ${out} ${src}')
244+
if r.exit_code != 0 {
245+
eprintln(' FAILED')
246+
eprintln(r.output)
247+
exit(1)
248+
}
249+
println(' ok')
250+
}
251+
252+
println('')
253+
254+
// Run both
255+
mut data := [2]BenchData{}
256+
for i, bin in [tmp_boehm, tmp_vgc] {
257+
gc_name := if i == 0 { 'boehm' } else { 'vgc' }
258+
print('running ${gc_name}...')
259+
flush_stdout()
260+
r := os.execute('${bin} --run-workload')
261+
if r.exit_code != 0 {
262+
eprintln(' FAILED')
263+
eprintln(r.output)
264+
exit(1)
265+
}
266+
data[i] = parse_output(r.output)
267+
println(' done')
268+
}
269+
270+
// Print comparison table
271+
println('')
272+
println(' ${rpad('test', 44)} ${lpad('boehm', 9)} ${lpad('vgc', 9)} ${lpad('ratio',
273+
9)}')
274+
println(' ${'—'.repeat(44)} ${'—'.repeat(9)} ${'—'.repeat(9)} ${'—'.repeat(9)}')
275+
276+
for ti in 0 .. 5 {
277+
mut boehm_vals := data[0].times[ti].clone()
278+
mut vgc_vals := data[1].times[ti].clone()
279+
mb := median(mut boehm_vals)
280+
mv := median(mut vgc_vals)
281+
ratio := if mb > 0 { f64(mv) / f64(mb) } else { 0.0 }
282+
winner := if ratio < 0.95 {
283+
' <-- vgc'
284+
} else if ratio > 1.05 {
285+
' <-- boehm'
286+
} else {
287+
''
288+
}
289+
label := '${ratio:.2f}x${winner}'
290+
println(' ${rpad(test_name(ti), 44)} ${lpad('${mb} ms', 9)} ${lpad('${mv} ms',
291+
9)} ${lpad(label, 9)}')
292+
}
293+
294+
// Heap usage
295+
println('')
296+
println(' heap usage:')
297+
println(' boehm: ${data[0].heap / 1024} KB allocated, ${data[0].free / 1024} KB free')
298+
println(' vgc: ${data[1].heap / 1024} KB allocated, ${data[1].free / 1024} KB free')
299+
println('')
300+
println(' (ratio < 1.0 = vgc faster, > 1.0 = boehm faster)')
301+
302+
// Cleanup
303+
os.rm(tmp_boehm) or {}
304+
os.rm(tmp_vgc) or {}
305+
}
306+
307+
fn main() {
308+
if '--run-workload' in os.args {
309+
// We were launched by the runner - execute the workload
310+
run_workload()
311+
} else {
312+
// Act as runner: compile with both GCs and compare
313+
run_comparison()
314+
}
96315
}

0 commit comments

Comments
 (0)