Skip to content

Commit 7c44a01

Browse files
committed
cgen: lower Boehm GC free-space divisor to 1 for the opt modes (fix #27555)
Allocation-heavy multithreaded HTTP servers regressed on the default GC after thread-local allocation (#27544) removed the allocator-lock bottleneck: with the fast path no longer serializing on the global lock, the remaining bottleneck is Boehm's stop-the-world collections, which each pause every worker thread. The optimized GC modes kept the heap small via GC_set_free_space_divisor(2), which makes those collections frequent. Lower it to 1 so the heap may grow to roughly the live set before collecting, cutting stop-the-world pauses ~3x. It is still emitted before GC_INIT(), so a user GC_FREE_SPACE_DIVISOR env var overrides it and memory-constrained programs can raise it again. Measured on a 10-core machine, single-allocation JSON workload: - multithreaded: +21% throughput (1.79M -> 2.17M rps) - single-threaded: 3.3x fewer collections (7277 -> 2224 per 2M requests)
1 parent 351709f commit 7c44a01

2 files changed

Lines changed: 34 additions & 2 deletions

File tree

vlib/v/gen/c/cmain.v

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,13 @@ fn (mut g Gen) gen_boehm_gc_init() {
161161
}
162162
g.writeln('\tGC_set_pages_executable(0);')
163163
if g.pref.gc_mode in [.boehm_full_opt, .boehm_incr_opt] {
164-
g.writeln('\tGC_set_free_space_divisor(2);')
164+
// Let the heap grow to roughly the live set before collecting, instead of
165+
// keeping it small. A smaller divisor means rarer stop-the-world pauses;
166+
// with thread-local allocation those pauses (not the allocator lock) are
167+
// what serializes worker threads on allocation-heavy multithreaded servers
168+
// (issue #27555). Emitted before GC_INIT(), so a user `GC_FREE_SPACE_DIVISOR`
169+
// env var still wins and memory-constrained programs can raise it again.
170+
g.writeln('\tGC_set_free_space_divisor(1);')
165171
}
166172
if g.pref.use_coroutines {
167173
g.writeln('\tGC_allow_register_threads();')
@@ -190,7 +196,9 @@ fn (mut g Gen) gen_shared_library_boehm_init() {
190196
// equivalent restriction applies to Windows DLLs.
191197
g.writeln('\tGC_set_pages_executable(0);')
192198
if g.pref.gc_mode in [.boehm_full_opt, .boehm_incr_opt] {
193-
g.writeln('\tGC_set_free_space_divisor(2);')
199+
// See gen_boehm_gc_init: rarer stop-the-world pauses for the opt modes,
200+
// overridable via the `GC_FREE_SPACE_DIVISOR` env var (issue #27555).
201+
g.writeln('\tGC_set_free_space_divisor(1);')
194202
}
195203
g.writeln('\tGC_INIT();')
196204
g.writeln('\tGC_register_displacement(sizeof(void*));')
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// Regression test for issue #27555: on the optimized `-gc` modes (the default
2+
// `boehm_full_opt`), `main()` calls `GC_set_free_space_divisor(1)` before
3+
// `GC_INIT()`, so the heap is allowed to grow to roughly the live set before
4+
// collecting. That keeps stop-the-world collections rare, which with thread-local
5+
// allocation is what otherwise serializes worker threads on allocation-heavy
6+
// multithreaded HTTP servers. An explicit `GC_FREE_SPACE_DIVISOR` env var is
7+
// applied by `GC_INIT()` afterwards and wins, so the test skips when one is set.
8+
import os
9+
10+
fn C.GC_get_free_space_divisor() usize
11+
12+
fn test_free_space_divisor_is_tuned_for_throughput() {
13+
$if gcboehm_opt ? {
14+
if os.getenv('GC_FREE_SPACE_DIVISOR') != '' {
15+
eprintln('skipping: explicit GC_FREE_SPACE_DIVISOR override is set')
16+
assert true
17+
return
18+
}
19+
assert C.GC_get_free_space_divisor() == 1
20+
} $else {
21+
eprintln('skipping: not an optimized boehm GC build')
22+
assert true
23+
}
24+
}

0 commit comments

Comments
 (0)