From cd213a17c4963eb35f9c52706c0d8731a89eee0c Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Fri, 26 Jun 2026 12:12:33 +0300 Subject: [PATCH] cgen: lower Boehm GC free-space divisor to 1 for the opt modes (fix #27555) (#27560) --- vlib/v/gen/c/cmain.v | 18 ++++++++++++-- vlib/v/gen/c/coutput_test.v | 4 ++++ .../boehm_default_startup.c.must_have | 2 +- vlib/v/tests/gc_free_space_divisor_test.v | 24 +++++++++++++++++++ 4 files changed, 45 insertions(+), 3 deletions(-) create mode 100644 vlib/v/tests/gc_free_space_divisor_test.v diff --git a/vlib/v/gen/c/cmain.v b/vlib/v/gen/c/cmain.v index 7970e6a74..2a2817b54 100644 --- a/vlib/v/gen/c/cmain.v +++ b/vlib/v/gen/c/cmain.v @@ -161,7 +161,13 @@ fn (mut g Gen) gen_boehm_gc_init() { } g.writeln('\tGC_set_pages_executable(0);') if g.pref.gc_mode in [.boehm_full_opt, .boehm_incr_opt] { - g.writeln('\tGC_set_free_space_divisor(2);') + // Let the heap grow to roughly the live set before collecting, instead of + // keeping it small. A smaller divisor means rarer stop-the-world pauses; + // with thread-local allocation those pauses (not the allocator lock) are + // what serializes worker threads on allocation-heavy multithreaded servers + // (issue #27555). Emitted before GC_INIT(), so a user `GC_FREE_SPACE_DIVISOR` + // env var still wins and memory-constrained programs can raise it again. + g.writeln('\tGC_set_free_space_divisor(1);') } if g.pref.use_coroutines { g.writeln('\tGC_allow_register_threads();') @@ -190,7 +196,15 @@ fn (mut g Gen) gen_shared_library_boehm_init() { // equivalent restriction applies to Windows DLLs. g.writeln('\tGC_set_pages_executable(0);') if g.pref.gc_mode in [.boehm_full_opt, .boehm_incr_opt] { - g.writeln('\tGC_set_free_space_divisor(2);') + // See gen_boehm_gc_init: rarer stop-the-world pauses for the opt modes, + // overridable via the `GC_FREE_SPACE_DIVISOR` env var (issue #27555). + // Only tune when this library is the one bringing up the collector. If the + // host process already initialized Boehm, the local GC_INIT() below is a + // no-op that will not re-read the env var, so setting the divisor here would + // silently override the host's process-wide value (and its env override). + g.writeln('\tif (!GC_is_init_called()) {') + g.writeln('\t\tGC_set_free_space_divisor(1);') + g.writeln('\t}') } g.writeln('\tGC_INIT();') g.writeln('\tGC_register_displacement(sizeof(void*));') diff --git a/vlib/v/gen/c/coutput_test.v b/vlib/v/gen/c/coutput_test.v index 65e7a69e0..1a51494a8 100644 --- a/vlib/v/gen/c/coutput_test.v +++ b/vlib/v/gen/c/coutput_test.v @@ -579,6 +579,10 @@ fn test_user_defined_windows_dllmain_disables_generated_entrypoint() { ensure_compilation_succeeded(compilation, cmd) assert compilation.output.contains('void _vinit_caller() {') assert compilation.output.contains('GC_set_pages_executable(0);') + // The shared-library GC tuning (issue #27555) must stay guarded, so loading + // the library into an already-GC-initialized host does not clobber the host's + // process-wide free-space divisor (its local GC_INIT() would be a no-op). + assert compilation.output.contains('if (!GC_is_init_called()) {') assert compilation.output.contains('GC_INIT();') assert compilation.output.contains('DllMain(') assert compilation.output.contains('_vinit_caller();') diff --git a/vlib/v/gen/c/testdata/boehm_default_startup.c.must_have b/vlib/v/gen/c/testdata/boehm_default_startup.c.must_have index 1122b4253..725a90bb1 100644 --- a/vlib/v/gen/c/testdata/boehm_default_startup.c.must_have +++ b/vlib/v/gen/c/testdata/boehm_default_startup.c.must_have @@ -1,2 +1,2 @@ -GC_set_free_space_divisor(2); +GC_set_free_space_divisor(1); GC_INIT(); diff --git a/vlib/v/tests/gc_free_space_divisor_test.v b/vlib/v/tests/gc_free_space_divisor_test.v new file mode 100644 index 000000000..4d114778c --- /dev/null +++ b/vlib/v/tests/gc_free_space_divisor_test.v @@ -0,0 +1,24 @@ +// Regression test for issue #27555: on the optimized `-gc` modes (the default +// `boehm_full_opt`), `main()` calls `GC_set_free_space_divisor(1)` before +// `GC_INIT()`, so the heap is allowed to grow to roughly the live set before +// collecting. That keeps stop-the-world collections rare, which with thread-local +// allocation is what otherwise serializes worker threads on allocation-heavy +// multithreaded HTTP servers. An explicit `GC_FREE_SPACE_DIVISOR` env var is +// applied by `GC_INIT()` afterwards and wins, so the test skips when one is set. +import os + +fn C.GC_get_free_space_divisor() usize + +fn test_free_space_divisor_is_tuned_for_throughput() { + $if gcboehm_opt ? { + if os.getenv('GC_FREE_SPACE_DIVISOR') != '' { + eprintln('skipping: explicit GC_FREE_SPACE_DIVISOR override is set') + assert true + return + } + assert C.GC_get_free_space_divisor() == 1 + } $else { + eprintln('skipping: not an optimized boehm GC build') + assert true + } +} -- 2.39.5