v / bench / bench_gc.v
314 lines · 281 sloc · 6.97 KB · 8e35f4d9848f7ad35d857a187dddbfd2eca5e19d
Raw
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
9module main
10
11import os
12import time
13
14const n_allocs = 1_000_000
15const n_iters = 5
16const tree_depth = 18
17const tree_rounds = 10
18const array_rounds = 100
19const array_size = 100_000
20
21struct Node {
22mut:
23 left &Node = unsafe { nil }
24 right &Node = unsafe { nil }
25 value int
26}
27
28fn 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
40// Allocate many small objects (typical GC workload)
41fn bench_small_allocs() (i64, int) {
42 sw := time.new_stopwatch()
43 mut sum := 0
44 for _ in 0 .. n_allocs {
45 s := 'hello world ${sum}'
46 sum += s.len
47 }
48 return sw.elapsed().milliseconds(), sum
49}
50
51// Build a binary tree (tests pointer-heavy allocation)
52fn make_tree(depth int) &Node {
53 if depth <= 0 {
54 return &Node{
55 value: 1
56 }
57 }
58 return &Node{
59 left: make_tree(depth - 1)
60 right: make_tree(depth - 1)
61 value: depth
62 }
63}
64
65fn tree_sum(n &Node) int {
66 if n == unsafe { nil } {
67 return 0
68 }
69 return n.value + tree_sum(n.left) + tree_sum(n.right)
70}
71
72fn bench_tree() (i64, int) {
73 sw := time.new_stopwatch()
74 mut total := 0
75 for _ in 0 .. tree_rounds {
76 t := make_tree(tree_depth)
77 total += tree_sum(t)
78 }
79 return sw.elapsed().milliseconds(), total
80}
81
82// Allocate and discard arrays (tests realloc / growing)
83fn bench_arrays() (i64, int) {
84 sw := time.new_stopwatch()
85 mut total := 0
86 for _ in 0 .. array_rounds {
87 mut arr := []int{cap: 16}
88 for j in 0 .. array_size {
89 arr << j
90 }
91 total += arr.len
92 }
93 return sw.elapsed().milliseconds(), total
94}
95
96// Allocate many maps
97fn 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
106 }
107 return sw.elapsed().milliseconds(), total
108}
109
110// Mixed workload: interleave allocations and collections
111fn 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
130 }
131 return sw.elapsed().milliseconds(), total
132}
133
134// --- Workload runner (compiled with a specific GC) ---
135
136fn 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 }
152 usage := gc_heap_usage()
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
159struct BenchData {
160mut:
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
167fn 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
178fn 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
185fn 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
193fn 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
215fn rpad(s string, width int) string {
216 if s.len >= width {
217 return s
218 }
219 return s + ' '.repeat(width - s.len)
220}
221
222fn lpad(s string, width int) string {
223 if s.len >= width {
224 return s
225 }
226 return ' '.repeat(width - s.len) + s
227}
228
229fn 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', 9)}')
273 println(' ${''.repeat(44)} ${''.repeat(9)} ${''.repeat(9)} ${''.repeat(9)}')
274
275 for ti in 0 .. 5 {
276 mut boehm_vals := data[0].times[ti].clone()
277 mut vgc_vals := data[1].times[ti].clone()
278 mb := median(mut boehm_vals)
279 mv := median(mut vgc_vals)
280 ratio := if mb > 0 { f64(mv) / f64(mb) } else { 0.0 }
281 winner := if ratio < 0.95 {
282 ' <-- vgc'
283 } else if ratio > 1.05 {
284 ' <-- boehm'
285 } else {
286 ''
287 }
288 label := '${ratio:.2f}x${winner}'
289 println(' ${rpad(test_name(ti), 44)} ${lpad('${mb} ms', 9)} ${lpad('${mv} ms', 9)} ${lpad(label,
290 9)}')
291 }
292
293 // Heap usage
294 println('')
295 println(' heap usage:')
296 println(' boehm: ${data[0].heap / 1024} KB allocated, ${data[0].free / 1024} KB free')
297 println(' vgc: ${data[1].heap / 1024} KB allocated, ${data[1].free / 1024} KB free')
298 println('')
299 println(' (ratio < 1.0 = vgc faster, > 1.0 = boehm faster)')
300
301 // Cleanup
302 os.rm(tmp_boehm) or {}
303 os.rm(tmp_vgc) or {}
304}
305
306fn main() {
307 if '--run-workload' in os.args {
308 // We were launched by the runner - execute the workload
309 run_workload()
310 } else {
311 // Act as runner: compile with both GCs and compare
312 run_comparison()
313 }
314}
315