Skip to content

Commit 7ef1ed9

Browse files
committed
gc: add boehm thread-local alloc opt-out
1 parent 82e947b commit 7ef1ed9

5 files changed

Lines changed: 104 additions & 18 deletions

File tree

‎vlib/builtin/builtin_d_gcboehm.c.v‎

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,24 @@ module builtin
22

33
$if !no_gc_threads ? {
44
#flag -DGC_THREADS=1
5-
// Enable Boehm's thread-local allocation: each registered thread gets its own
6-
// small-object free lists, so concurrent `GC_malloc` on the fast path no longer
7-
// serializes on the global allocator lock. Without it, allocation does not scale
8-
// across cores (16 cores ≈ 1 core aggregate). V's spawned threads are registered
9-
// via the `pthread_create` -> `GC_pthread_create` redirect, so their free lists
10-
// are set up automatically. TLA requires GC_THREADS (defined just above).
11-
//
12-
// This flag only matters for the bundled `gc.c` that V compiles from source (the
13-
// `-prod`/no-prebuilt-archive branches below): its amalgamation was generated with
14-
// `--enable-thread-local-alloc=no` (see thirdparty/libgc/amalgamation.txt), so the
15-
// flag turns TLA back on. The prebuilt `thirdparty/tcc/lib/libgc.a`/`.dylib` used by
16-
// the default fast path is already built with TLA (bdwgc enables it by default with
17-
// `--enable-threads=pthreads`; see thirdparty/build_scripts/*_bdwgc.sh), and a system
18-
// libgc that V merely links against is unaffected. So both bundled GC paths end up
19-
// thread-local-alloc enabled. See issues #27486 and #27488.
20-
#flag -DTHREAD_LOCAL_ALLOC=1
5+
$if !no_gc_thread_local_alloc ? {
6+
// Enable Boehm's thread-local allocation: each registered thread gets
7+
// its own small-object free lists, so concurrent `GC_malloc` on the fast
8+
// path no longer serializes on the global allocator lock. Without it,
9+
// allocation does not scale across cores. V's spawned threads are
10+
// registered via the `pthread_create` -> `GC_pthread_create` redirect,
11+
// so their free lists are set up automatically. TLA requires GC_THREADS.
12+
//
13+
// This flag only matters for the bundled `gc.c` that V compiles from
14+
// source: its amalgamation was generated with
15+
// `--enable-thread-local-alloc=no` (see thirdparty/libgc/amalgamation.txt),
16+
// so the flag turns TLA back on. The prebuilt
17+
// `thirdparty/tcc/lib/libgc.a`/`.dylib` archives are already built with
18+
// TLA. `-d no_gc_thread_local_alloc` keeps GC_THREADS but omits this flag
19+
// and makes the compiler prefer the source-built bundled libgc path.
20+
// See issues #27486, #27488 and #27553.
21+
#flag -DTHREAD_LOCAL_ALLOC=1
22+
}
2123
}
2224

2325
$if use_bundled_libgc ? {

‎vlib/v/builder/gc_flags_test.v‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,3 +46,15 @@ fn test_linux_musl_tcc_boehm_uses_system_libgc() {
4646
assert res.output.contains('-lgc')
4747
assert !res.output.contains('thirdparty/tcc/lib/libgc.a')
4848
}
49+
50+
fn test_no_gc_thread_local_alloc_uses_source_libgc_without_tla_define() {
51+
source_path := os.join_path(@VEXEROOT, 'examples', 'hello_world.v')
52+
cmd := '${os.quoted_path(@VEXE)} -dump-c-flags - -d no_gc_thread_local_alloc ${os.quoted_path(source_path)}'
53+
res := execute_without_vflags(cmd)
54+
assert res.exit_code == 0, res.output
55+
normalized := res.output.replace('\\', '/')
56+
assert !normalized.contains('thirdparty/tcc/lib/libgc')
57+
assert !normalized.contains('\n-lgc\n')
58+
assert normalized.contains('-D GC_THREADS=1')
59+
assert !normalized.contains('THREAD_LOCAL_ALLOC')
60+
}

‎vlib/v/help/build/build-c.txt‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,25 @@ see also `v help build`.
233233
collector is conservative, and it may affect program speed if it does many small
234234
allocations in a loop.
235235

236+
V enables Boehm thread-local allocation by default for better allocation
237+
scaling on multi-threaded programs. It can increase RSS in long-running
238+
services because each thread keeps local small-object free lists.
239+
240+
Boehm reads these environment variables during GC_INIT(), so they can be
241+
set per run before the collector starts, without rebuilding the V program:
242+
GC_MAXIMUM_HEAP_SIZE Cap the heap size in bytes.
243+
GC_FREE_SPACE_DIVISOR Higher values collect more often and can reduce
244+
spare heap; lower values favor throughput.
245+
GC_MARKERS Set the number of parallel marker threads.
246+
247+
To compile V's bundled source copy of Boehm without thread-local
248+
allocation, use `-d no_gc_thread_local_alloc`. This keeps GC_THREADS
249+
enabled, but skips THREAD_LOCAL_ALLOC. Since this requires compiling the
250+
bundled libgc source, V uses the platform C compiler by default instead
251+
of bundled TCC. An explicit `-cc` still wins. The option does not change
252+
a system libgc selected with `-d dynamic_boehm`; use a system libgc built
253+
without thread-local allocation for that case.
254+
236255
The option `-gc boehm_leak` is intended for leak detection in
237256
manual memory management. The function `gc_check_leaks()`
238257
can be called to get detection results. This function is a no-op

‎vlib/v/pref/default.v‎

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,23 @@ fn (mut p Preferences) prefer_openssl_for_bsd_tinyc() {
184184
}
185185
}
186186

187+
fn (p &Preferences) needs_source_boehm_without_thread_local_alloc() bool {
188+
return p.gc_mode in [.boehm_full, .boehm_incr, .boehm_full_opt, .boehm_incr_opt, .boehm_leak]
189+
&& 'no_gc_thread_local_alloc' in p.compile_defines_all
190+
&& 'dynamic_boehm' !in p.compile_defines_all
191+
}
192+
193+
fn (mut p Preferences) prefer_source_boehm_without_thread_local_alloc() {
194+
if !p.needs_source_boehm_without_thread_local_alloc()
195+
|| 'use_bundled_libgc' in p.compile_defines_all {
196+
return
197+
}
198+
// The prebuilt bundled libgc archives are built with thread-local allocation.
199+
// Use the bundled source path instead, so builtin_d_gcboehm.c.v can omit
200+
// -DTHREAD_LOCAL_ALLOC while still keeping GC_THREADS enabled.
201+
p.parse_define('use_bundled_libgc')
202+
}
203+
187204
// fill_with_defaults initializes unset preferences and derives build options from them.
188205
pub fn (mut p Preferences) fill_with_defaults() {
189206
p.setup_os_and_arch_when_not_explicitly_set()
@@ -273,6 +290,7 @@ pub fn (mut p Preferences) fill_with_defaults() {
273290
}
274291
p.ccompiler_type = cc_from_string(p.ccompiler)
275292
p.normalize_gc_defaults_for_resolved_ccompiler()
293+
p.prefer_source_boehm_without_thread_local_alloc()
276294
p.is_test = p.path.ends_with('_test.v') || p.path.ends_with('_test.vv')
277295
|| p.path.all_before_last('.v').all_before_last('.').ends_with('_test')
278296
p.is_vsh = p.path.ends_with('.vsh') || p.raw_vsh_tmp_prefix != ''
@@ -449,6 +467,12 @@ fn (mut p Preferences) try_to_use_tcc_by_default() {
449467
if p.prealloc {
450468
return
451469
}
470+
// -d no_gc_thread_local_alloc forces the bundled source libgc path. The
471+
// bundled tcc cannot compile that source reliably, so use the platform C
472+
// compiler by default. An explicit -cc still wins.
473+
if p.needs_source_boehm_without_thread_local_alloc() {
474+
return
475+
}
452476
// use an optimizing compiler (i.e. gcc or clang) on -prod mode
453477
if p.is_prod {
454478
return
@@ -546,10 +570,10 @@ fn (mut p Preferences) clear_gc_options() {
546570
pub fn (mut p Preferences) default_c_compiler() {
547571
// TODO: fix $if after 'string'
548572
$if windows {
549-
// -prealloc and -prod intentionally avoid the bundled tcc (see
573+
// These modes intentionally avoid bundled tcc (see
550574
// try_to_use_tcc_by_default); preserve that here so the Windows fallback
551575
// does not silently re-select an incompatible compiler.
552-
if p.prealloc || p.is_prod {
576+
if p.prealloc || p.is_prod || p.needs_source_boehm_without_thread_local_alloc() {
553577
p.ccompiler = 'gcc'
554578
return
555579
}

‎vlib/v/pref/pref_test.v‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -383,6 +383,35 @@ fn test_prealloc_overrides_explicit_gc_selection() {
383383
assert prefs.build_options.join(' ').contains('-gc none')
384384
}
385385

386+
fn test_no_gc_thread_local_alloc_prefers_source_bundled_boehm() {
387+
target := os.join_path(vroot, 'examples', 'hello_world.v')
388+
prefs, _ := pref.parse_args_and_show_errors([], ['', '-d', 'no_gc_thread_local_alloc', target],
389+
false)
390+
assert prefs.gc_mode == .boehm_full_opt
391+
assert 'no_gc_thread_local_alloc' in prefs.compile_defines_all
392+
assert 'use_bundled_libgc' in prefs.compile_defines_all
393+
assert !prefs.ccompiler.contains('tcc')
394+
assert prefs.build_options.contains('-d use_bundled_libgc')
395+
}
396+
397+
fn test_no_gc_thread_local_alloc_does_not_force_boehm_with_gc_none() {
398+
target := os.join_path(vroot, 'examples', 'hello_world.v')
399+
prefs, _ := pref.parse_args_and_show_errors([], ['', '-gc', 'none', '-d',
400+
'no_gc_thread_local_alloc', target], false)
401+
assert prefs.gc_mode == .no_gc
402+
assert 'no_gc_thread_local_alloc' in prefs.compile_defines_all
403+
assert 'use_bundled_libgc' !in prefs.compile_defines_all
404+
}
405+
406+
fn test_no_gc_thread_local_alloc_keeps_explicit_dynamic_boehm() {
407+
target := os.join_path(vroot, 'examples', 'hello_world.v')
408+
prefs, _ := pref.parse_args_and_show_errors([], ['', '-d', 'dynamic_boehm', '-d',
409+
'no_gc_thread_local_alloc', target], false)
410+
assert prefs.gc_mode == .boehm_full_opt
411+
assert 'dynamic_boehm' in prefs.compile_defines_all
412+
assert 'use_bundled_libgc' !in prefs.compile_defines_all
413+
}
414+
386415
fn stale_windows_gc_prefs(gc_set_by_flag bool) pref.Preferences {
387416
mut prefs := pref.Preferences{
388417
os: .windows

0 commit comments

Comments
 (0)