From 0a28caa84d8e40354ae81d291d321bbb74abe97e Mon Sep 17 00:00:00 2001 From: Eelco Dolstra Date: Tue, 25 Aug 2026 14:39:37 +0200 Subject: [PATCH] Serve GC allocations up to 1520 bytes from the thread-local freelists MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Profiling 'nix search nixpkgs --no-eval-cache' showed that ~65% of all futex waits came from the Boehm GC, the largest share being the global allocation lock (GC_allocate_ml): with the default GC_TINY_FREELISTS of 25, only allocations up to 384 bytes are served from the per-thread freelists, and everything larger takes the global lock. Nixpkgs evaluation does ~755k such allocations per search — e.g. a typical derivation attrset (~46 attrs) is a 752-byte Bindings, of which one is allocated per package — all serialized on the lock across the worker threads. Building bdwgc with GC_TINY_FREELISTS=96 (thread-local up to 1520 bytes) eliminates 93% of the global-lock allocations (755k -> 56k; what remains is essentially the >16 KiB attrset giants), halves the context-switch count at 8-16 workers, and makes the search ~10% faster at 24 workers (~3.7s -> ~3.3s). The per-thread memory cost is at most a partial heap block per size class and object kind. Also add a patch (from the bdwgc "nix-patches" branch) that makes gctest's fixed "unexpected heap growth" limit scale with GC_TINY_FREELISTS, since the larger freelists exceed it by design. Assisted-by: Claude Fable 5 --- packaging/dependencies.nix | 19 ++++++---- ...gc-gctest-tiny-freelists-heap-growth.patch | 38 +++++++++++++++++++ 2 files changed, 50 insertions(+), 7 deletions(-) create mode 100644 packaging/patches/boehmgc-gctest-tiny-freelists-heap-growth.patch diff --git a/packaging/dependencies.nix b/packaging/dependencies.nix index 480c4079e954..180e19433929 100644 --- a/packaging/dependencies.nix +++ b/packaging/dependencies.nix @@ -36,13 +36,10 @@ scope: { inherit stdenv; }).overrideAttrs (attrs: { - # Reduce contention on the GC allocation lock during parallel - # evaluation by handing out multiple heap blocks worth of - # objects per lock acquisition in GC_generic_malloc_many(). - # The default batch size is set via GC_MANY_BLOCKS_DEFAULT - # below and can be overridden at runtime through the - # GC_MALLOC_MANY_BLOCKS environment variable. - patches = (attrs.patches or [ ]) ++ [ ./patches/boehmgc-batch-malloc-many.patch ]; + patches = (attrs.patches or [ ]) ++ [ + ./patches/boehmgc-batch-malloc-many.patch + ./patches/boehmgc-gctest-tiny-freelists-heap-growth.patch + ]; env = (attrs.env or { }) // { # Increase the initial mark stack size to avoid stack @@ -55,6 +52,14 @@ scope: { [ "-DINITIAL_MARK_STACK_SIZE=1048576" "-DGC_MANY_BLOCKS_DEFAULT=64" + # Serve allocations up to 1520 bytes (95 granules) from + # the per-thread freelists instead of taking the global + # allocation lock. The default (25, i.e. <= 384 bytes) is + # too small for parallel evaluation: e.g. a typical + # derivation attrset (~46 attrs) is a 752-byte Bindings, + # of which nixpkgs evaluation does hundreds of thousands, + # all serialized on GC_allocate_ml. + "-DGC_TINY_FREELISTS=96" ] # For some reason that is not clear, it is wanting to use libgcc_eh which is not available. # Force this to be built with compiler-rt & libunwind over libgcc_eh works. diff --git a/packaging/patches/boehmgc-gctest-tiny-freelists-heap-growth.patch b/packaging/patches/boehmgc-gctest-tiny-freelists-heap-growth.patch new file mode 100644 index 000000000000..a7cd51d6945d --- /dev/null +++ b/packaging/patches/boehmgc-gctest-tiny-freelists-heap-growth.patch @@ -0,0 +1,38 @@ +From 7cd4f090acc0e801502f918060106bba926c7153 Mon Sep 17 00:00:00 2001 +From: Eelco Dolstra +Date: Tue, 25 Aug 2026 14:37:18 +0200 +Subject: [PATCH] Scale gctest's heap growth limit with GC_TINY_FREELISTS + +The 'unexpected heap growth' check in check_heap_stats() uses a fixed +limit that is exceeded when the collector is built with a larger +GC_TINY_FREELISTS: bigger thread-local free lists cause each thread to +retain more nearly-empty heap blocks (up to one per size class and +object kind) and increase block-level fragmentation. Scale the limit +proportionally so that gctest passes with such configurations. + +Assisted-by: Claude Fable 5 +--- + tests/test.c | 7 +++++++ + 1 file changed, 7 insertions(+) + +diff --git a/tests/test.c b/tests/test.c +index 0ae3f95b..b62699fa 100644 +--- a/tests/test.c ++++ b/tests/test.c +@@ -1753,6 +1753,13 @@ void check_heap_stats(void) + # endif + # ifdef MEMORY_SANITIZER + max_heap_sz += max_heap_sz / 4; ++# endif ++# if defined(GC_TINY_FREELISTS) && GC_TINY_FREELISTS > 25 ++ /* Larger thread-local free lists cause each thread to retain */ ++ /* more nearly-empty heap blocks (up to one per size class and */ ++ /* object kind) and increase block-level fragmentation, so */ ++ /* allow the heap to grow proportionally. */ ++ max_heap_sz = max_heap_sz / 25 * GC_TINY_FREELISTS; + # endif + max_heap_sz *= n_tests; + # if defined(USE_MMAP) || defined(MSWIN32) +-- +2.54.0 +