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
412import time
513
614const n_allocs = 1_000_000
715const 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
921struct Node {
1022mut :
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 :.2 f }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