From db39f2e0bc5bb212975a164c3536f441d7e9efbe Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Sat, 27 Jun 2026 13:02:39 +0300 Subject: [PATCH] gc: add boehm thread-local alloc opt-out (#27572) --- vlib/builtin/builtin_d_gcboehm.c.v | 34 ++++++++++++++++-------------- vlib/v/builder/gc_flags_test.v | 12 +++++++++++ vlib/v/help/build/build-c.txt | 19 +++++++++++++++++ vlib/v/pref/default.v | 28 ++++++++++++++++++++++-- vlib/v/pref/pref_test.v | 29 +++++++++++++++++++++++++ 5 files changed, 104 insertions(+), 18 deletions(-) diff --git a/vlib/builtin/builtin_d_gcboehm.c.v b/vlib/builtin/builtin_d_gcboehm.c.v index 40c2ad17b..816fbe41f 100644 --- a/vlib/builtin/builtin_d_gcboehm.c.v +++ b/vlib/builtin/builtin_d_gcboehm.c.v @@ -2,22 +2,24 @@ module builtin $if !no_gc_threads ? { #flag -DGC_THREADS=1 - // Enable Boehm's thread-local allocation: each registered thread gets its own - // small-object free lists, so concurrent `GC_malloc` on the fast path no longer - // serializes on the global allocator lock. Without it, allocation does not scale - // across cores (16 cores ≈ 1 core aggregate). V's spawned threads are registered - // via the `pthread_create` -> `GC_pthread_create` redirect, so their free lists - // are set up automatically. TLA requires GC_THREADS (defined just above). - // - // This flag only matters for the bundled `gc.c` that V compiles from source (the - // `-prod`/no-prebuilt-archive branches below): its amalgamation was generated with - // `--enable-thread-local-alloc=no` (see thirdparty/libgc/amalgamation.txt), so the - // flag turns TLA back on. The prebuilt `thirdparty/tcc/lib/libgc.a`/`.dylib` used by - // the default fast path is already built with TLA (bdwgc enables it by default with - // `--enable-threads=pthreads`; see thirdparty/build_scripts/*_bdwgc.sh), and a system - // libgc that V merely links against is unaffected. So both bundled GC paths end up - // thread-local-alloc enabled. See issues #27486 and #27488. - #flag -DTHREAD_LOCAL_ALLOC=1 + $if !no_gc_thread_local_alloc ? { + // Enable Boehm's thread-local allocation: each registered thread gets + // its own small-object free lists, so concurrent `GC_malloc` on the fast + // path no longer serializes on the global allocator lock. Without it, + // allocation does not scale across cores. V's spawned threads are + // registered via the `pthread_create` -> `GC_pthread_create` redirect, + // so their free lists are set up automatically. TLA requires GC_THREADS. + // + // This flag only matters for the bundled `gc.c` that V compiles from + // source: its amalgamation was generated with + // `--enable-thread-local-alloc=no` (see thirdparty/libgc/amalgamation.txt), + // so the flag turns TLA back on. The prebuilt + // `thirdparty/tcc/lib/libgc.a`/`.dylib` archives are already built with + // TLA. `-d no_gc_thread_local_alloc` keeps GC_THREADS but omits this flag + // and makes the compiler prefer the source-built bundled libgc path. + // See issues #27486, #27488 and #27553. + #flag -DTHREAD_LOCAL_ALLOC=1 + } } $if use_bundled_libgc ? { diff --git a/vlib/v/builder/gc_flags_test.v b/vlib/v/builder/gc_flags_test.v index 3ec1bbf52..8bc4f788e 100644 --- a/vlib/v/builder/gc_flags_test.v +++ b/vlib/v/builder/gc_flags_test.v @@ -46,3 +46,15 @@ fn test_linux_musl_tcc_boehm_uses_system_libgc() { assert res.output.contains('-lgc') assert !res.output.contains('thirdparty/tcc/lib/libgc.a') } + +fn test_no_gc_thread_local_alloc_uses_source_libgc_without_tla_define() { + source_path := os.join_path(@VEXEROOT, 'examples', 'hello_world.v') + cmd := '${os.quoted_path(@VEXE)} -dump-c-flags - -d no_gc_thread_local_alloc ${os.quoted_path(source_path)}' + res := execute_without_vflags(cmd) + assert res.exit_code == 0, res.output + normalized := res.output.replace('\\', '/') + assert !normalized.contains('thirdparty/tcc/lib/libgc') + assert !normalized.contains('\n-lgc\n') + assert normalized.contains('-D GC_THREADS=1') + assert !normalized.contains('THREAD_LOCAL_ALLOC') +} diff --git a/vlib/v/help/build/build-c.txt b/vlib/v/help/build/build-c.txt index ea9c6aed9..b32cc50f8 100644 --- a/vlib/v/help/build/build-c.txt +++ b/vlib/v/help/build/build-c.txt @@ -233,6 +233,25 @@ see also `v help build`. collector is conservative, and it may affect program speed if it does many small allocations in a loop. + V enables Boehm thread-local allocation by default for better allocation + scaling on multi-threaded programs. It can increase RSS in long-running + services because each thread keeps local small-object free lists. + + Boehm reads these environment variables during GC_INIT(), so they can be + set per run before the collector starts, without rebuilding the V program: + GC_MAXIMUM_HEAP_SIZE Cap the heap size in bytes. + GC_FREE_SPACE_DIVISOR Higher values collect more often and can reduce + spare heap; lower values favor throughput. + GC_MARKERS Set the number of parallel marker threads. + + To compile V's bundled source copy of Boehm without thread-local + allocation, use `-d no_gc_thread_local_alloc`. This keeps GC_THREADS + enabled, but skips THREAD_LOCAL_ALLOC. Since this requires compiling the + bundled libgc source, V uses the platform C compiler by default instead + of bundled TCC. An explicit `-cc` still wins. The option does not change + a system libgc selected with `-d dynamic_boehm`; use a system libgc built + without thread-local allocation for that case. + The option `-gc boehm_leak` is intended for leak detection in manual memory management. The function `gc_check_leaks()` can be called to get detection results. This function is a no-op diff --git a/vlib/v/pref/default.v b/vlib/v/pref/default.v index 871b1c3a1..df9585962 100644 --- a/vlib/v/pref/default.v +++ b/vlib/v/pref/default.v @@ -184,6 +184,23 @@ fn (mut p Preferences) prefer_openssl_for_bsd_tinyc() { } } +fn (p &Preferences) needs_source_boehm_without_thread_local_alloc() bool { + return p.gc_mode in [.boehm_full, .boehm_incr, .boehm_full_opt, .boehm_incr_opt, .boehm_leak] + && 'no_gc_thread_local_alloc' in p.compile_defines_all + && 'dynamic_boehm' !in p.compile_defines_all +} + +fn (mut p Preferences) prefer_source_boehm_without_thread_local_alloc() { + if !p.needs_source_boehm_without_thread_local_alloc() + || 'use_bundled_libgc' in p.compile_defines_all { + return + } + // The prebuilt bundled libgc archives are built with thread-local allocation. + // Use the bundled source path instead, so builtin_d_gcboehm.c.v can omit + // -DTHREAD_LOCAL_ALLOC while still keeping GC_THREADS enabled. + p.parse_define('use_bundled_libgc') +} + // fill_with_defaults initializes unset preferences and derives build options from them. pub fn (mut p Preferences) fill_with_defaults() { p.setup_os_and_arch_when_not_explicitly_set() @@ -273,6 +290,7 @@ pub fn (mut p Preferences) fill_with_defaults() { } p.ccompiler_type = cc_from_string(p.ccompiler) p.normalize_gc_defaults_for_resolved_ccompiler() + p.prefer_source_boehm_without_thread_local_alloc() p.is_test = p.path.ends_with('_test.v') || p.path.ends_with('_test.vv') || p.path.all_before_last('.v').all_before_last('.').ends_with('_test') 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() { if p.prealloc { return } + // -d no_gc_thread_local_alloc forces the bundled source libgc path. The + // bundled tcc cannot compile that source reliably, so use the platform C + // compiler by default. An explicit -cc still wins. + if p.needs_source_boehm_without_thread_local_alloc() { + return + } // use an optimizing compiler (i.e. gcc or clang) on -prod mode if p.is_prod { return @@ -546,10 +570,10 @@ fn (mut p Preferences) clear_gc_options() { pub fn (mut p Preferences) default_c_compiler() { // TODO: fix $if after 'string' $if windows { - // -prealloc and -prod intentionally avoid the bundled tcc (see + // These modes intentionally avoid bundled tcc (see // try_to_use_tcc_by_default); preserve that here so the Windows fallback // does not silently re-select an incompatible compiler. - if p.prealloc || p.is_prod { + if p.prealloc || p.is_prod || p.needs_source_boehm_without_thread_local_alloc() { p.ccompiler = 'gcc' return } diff --git a/vlib/v/pref/pref_test.v b/vlib/v/pref/pref_test.v index f3e955b78..9bea16442 100644 --- a/vlib/v/pref/pref_test.v +++ b/vlib/v/pref/pref_test.v @@ -383,6 +383,35 @@ fn test_prealloc_overrides_explicit_gc_selection() { assert prefs.build_options.join(' ').contains('-gc none') } +fn test_no_gc_thread_local_alloc_prefers_source_bundled_boehm() { + target := os.join_path(vroot, 'examples', 'hello_world.v') + prefs, _ := pref.parse_args_and_show_errors([], ['', '-d', 'no_gc_thread_local_alloc', target], + false) + assert prefs.gc_mode == .boehm_full_opt + assert 'no_gc_thread_local_alloc' in prefs.compile_defines_all + assert 'use_bundled_libgc' in prefs.compile_defines_all + assert !prefs.ccompiler.contains('tcc') + assert prefs.build_options.contains('-d use_bundled_libgc') +} + +fn test_no_gc_thread_local_alloc_does_not_force_boehm_with_gc_none() { + target := os.join_path(vroot, 'examples', 'hello_world.v') + prefs, _ := pref.parse_args_and_show_errors([], ['', '-gc', 'none', '-d', + 'no_gc_thread_local_alloc', target], false) + assert prefs.gc_mode == .no_gc + assert 'no_gc_thread_local_alloc' in prefs.compile_defines_all + assert 'use_bundled_libgc' !in prefs.compile_defines_all +} + +fn test_no_gc_thread_local_alloc_keeps_explicit_dynamic_boehm() { + target := os.join_path(vroot, 'examples', 'hello_world.v') + prefs, _ := pref.parse_args_and_show_errors([], ['', '-d', 'dynamic_boehm', '-d', + 'no_gc_thread_local_alloc', target], false) + assert prefs.gc_mode == .boehm_full_opt + assert 'dynamic_boehm' in prefs.compile_defines_all + assert 'use_bundled_libgc' !in prefs.compile_defines_all +} + fn stale_windows_gc_prefs(gc_set_by_flag bool) pref.Preferences { mut prefs := pref.Preferences{ os: .windows -- 2.39.5