| 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 |
| 12 | import time |
| 13 | |
| 14 | const n_allocs = 1_000_000 |
| 15 | 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 |
| 20 | |
| 21 | struct Node { |
| 22 | mut: |
| 23 | left &Node = unsafe { nil } |
| 24 | right &Node = unsafe { nil } |
| 25 | value int |
| 26 | } |
| 27 | |
| 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 | |
| 40 | // Allocate many small objects (typical GC workload) |
| 41 | fn 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) |
| 52 | fn 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 | |
| 65 | fn 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 | |
| 72 | fn 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) |
| 83 | fn 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 |
| 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 |
| 106 | } |
| 107 | return sw.elapsed().milliseconds(), total |
| 108 | } |
| 109 | |
| 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 |
| 130 | } |
| 131 | return sw.elapsed().milliseconds(), total |
| 132 | } |
| 133 | |
| 134 | // --- Workload runner (compiled with a specific GC) --- |
| 135 | |
| 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 | } |
| 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 | |
| 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', 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 | |
| 306 | fn 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 | |