Skip to content

Enable Boehm parallel marking (PARALLEL_MARK) - #1818

Open
dg1sbg wants to merge 2 commits into
clasp-developers:mainfrom
dg1sbg:perf/boehm-parallel-mark-pr
Open

Enable Boehm parallel marking (PARALLEL_MARK)#1818
dg1sbg wants to merge 2 commits into
clasp-developers:mainfrom
dg1sbg:perf/boehm-parallel-mark-pr

Conversation

@dg1sbg

@dg1sbg dg1sbg commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

PARALLEL_MARK was left undefined in boehm_config.h — the config-header template default — even though bdwgc's own CMake build defaults it ON and GC_THREADS / THREAD_LOCAL_ALLOC are already enabled here. Marking therefore ran single-threaded, and GC_mark_from is the dominant runtime cost (~25–30% in profiles) on multicore machines.

One line of actual change.

Why parallel markers are safe here

Parallel marking is only safe if the marking loop being parallelised is bdwgc's own. Clasp contributes no marking logic of its own:

  • Marking is Boehm-standard conservative marking — ALL_INTERIOR_POINTERS is 1 (boehm_config.h:5) and GC_register_displacement is called for GENERAL_TAG and CONS_TAG (boehmGarbageCollection.cc:210,218-219).
  • There are no custom mark procedures and no typed descriptors anywhere in the tree: GC_new_kind, GC_new_proc, GC_MAKE_PROC, GC_DS_PROC and GC_make_descriptor have zero occurrences outside src/bdwgc/.
  • Every allocation kind is one of the two builtins. gc_boot.cc:395-400 sets global_lisp_kind, global_cons_kind, global_class_kind, global_container_kind and global_code_kind to GC_I_NORMAL, and global_atomic_kind to GC_I_PTRFREE.
  • That holds on the precise path too — those assignments sit inside the walk == precise_info branch, where the per-stamp layout bitmaps are computed and validated (including the Cons_O bitmap assertion) but never become GC mark descriptors.

That the define reaches the collector

Worth stating explicitly, because boehm_config.h is included by only one Clasp source file and build.ninja contains no bdwgc compile rules — which makes it look inert. It isn't: src/gctools/mygc.c is the single translation unit Clasp builds bdwgc from, and it does

#include "clasp/gctools/boehm_config.h"
#include "extra/gc.c"

Confirmed empirically both ways after the change: GC_start_mark_threads becomes a defined symbol in libclasp (it is absent before), and with GC_PRINT_STATS=1 bdwgc reports Started 9 mark helper threads.

Measured effect

Marking cost on a ~1 GB live heap, median of 12 full collections. Both sides are the same binaryGC_MARKERS=1 disables parallel marking at runtime — so there is no rebuild and no second variable:

run 1 run 2 run 3
GC_MARKERS=1 (serial) 0.0702 s 0.0665 s 0.0662 s
default (parallel) 0.0187 s 0.0205 s 0.0187 s

Roughly 3.4–3.75× faster marking, with non-overlapping ranges across all three runs.

Testing

macOS arm64, :extensions (). Full regression suite and ansi-test across both GC variants and both build modes — the :bytecode rows are a real :build-mode :bytecode build (koga re-run, modules compiled :native nil, --base resolving to base.fasl), not merely a different image booted:

build-mode GC variant regression ansi-test
:native boehmprecise 1979 successes, 4 failures 27 failures, 0 unexpected
:native boehm 1977 successes, 4 failures
:bytecode boehmprecise 1979 successes, 4 failures 27 failures, 0 unexpected
:bytecode boehm 1977 successes, 4 failures 27 failures, 0 unexpected

Every failure set is the same four entries already in *expected-failures* (SBCL-CROSS-COMPILE-4, INCLUDE-LEVEL-2B, INCLUDE-LEVEL-3, TYPES-CLASSES-10), and the boehmprecise/:native run is test-by-test identical to the unmodified build. The bytecode image was additionally exercised under both GC variants from the :native build via --image, with the same results.

Concurrent-GC correctness stress: 8 threads churning 849 MB of mixed conses, strings and vectors while repeatedly walking a shared live tree and verifying its checksum — 400 full verifications during concurrent allocation, checksum intact throughout, all worker results exact.

One caveat for CI: PRINT.DOUBLE-FLOAT.RANDOM is flaky for reasons unrelated to this PR (see #1816) and can redden ansi-test at random, so its exit code alone is not a verdict — compare the failure lists.

dg1sbg added 2 commits August 2, 2026 16:04
PARALLEL_MARK was left undefined -- the config-header template default -- even
though bdwgc's own CMake build defaults it ON, and GC_THREADS and
THREAD_LOCAL_ALLOC are already enabled here. Marking therefore ran
single-threaded, and GC_mark_from is the dominant runtime cost (~25-30% in
profiles) on multicore machines.

Parallel markers are safe here because clasp contributes no marking logic of its
own. Marking is Boehm-standard conservative marking: ALL_INTERIOR_POINTERS is 1
(boehm_config.h) and GC_register_displacement is called for GENERAL_TAG and
CONS_TAG (boehmGarbageCollection.cc), and there are no custom mark procedures or
typed descriptors anywhere -- no GC_new_kind, GC_new_proc, GC_MAKE_PROC,
GC_DS_PROC or GC_make_descriptor. Every allocation kind is one of the two
builtins: gc_boot.cc sets global_lisp_kind, global_cons_kind, global_class_kind,
global_container_kind and global_code_kind to GC_I_NORMAL, and global_atomic_kind
to GC_I_PTRFREE. That holds on the precise path too -- those assignments sit
inside the `walk == precise_info` branch, where the per-stamp layout bitmaps are
computed and validated but never become GC mark descriptors. The marker threads
therefore parallelize bdwgc's own parallel-safe marking loop.

The define reaches the collector because src/gctools/mygc.c includes
clasp/gctools/boehm_config.h immediately before extra/gc.c, the single
translation unit clasp builds bdwgc from. Confirmed both ways after the change:
GC_start_mark_threads becomes a defined symbol in libclasp, and with
GC_PRINT_STATS=1 bdwgc reports "Started 9 mark helper threads".

Marking cost measured on the same binary with a ~1 GB live heap, toggling
GC_MARKERS at runtime (GC_MARKERS=1 disables parallel marking), median of 12
full collections, three runs each:

  GC_MARKERS=1   0.0702 s   0.0665 s   0.0662 s
  default        0.0187 s   0.0205 s   0.0187 s

roughly 3.4-3.75x faster, with non-overlapping ranges.

The regression suite is test-by-test identical to the unmodified build (1979
successes, the same four expected failures), ansi-test shows 27 failures with 0
unexpected, and an 8-thread stress that churns 849 MB while repeatedly walking a
shared live tree keeps its checksum intact.
boehm_config.h is a checked-in snapshot of bdwgc's autoconf-generated
config, and it was generated on macOS: it defined
HAVE_PTHREAD_SETNAME_NP_WITHOUT_TID unconditionally and left
HAVE_PTHREAD_SETNAME_NP_WITH_TID undefined on every platform.  glibc's
pthread_setname_np takes (pthread_t, const char *), so Linux got the
one-argument Darwin call:

  pthread_support.c:374:40: error: too few arguments to function call,
                                   expected 2, have 1
    (void)pthread_setname_np(name_buf);

That was harmless while the block was dead code -- set_marker_thread_name
is reachable only from GC_mark_thread, which exists only under
PARALLEL_MARK -- so enabling parallel marking in the preceding commit
broke every Linux build.

Guard each macro with the platform it describes.  Both are left
undefined elsewhere, where bdwgc falls back to
'#define set_marker_thread_name(id) (void)(id)'; that keeps the BSDs,
which spell the function pthread_set_name_np, on the existing no-op
path.  Naming marker threads is cosmetic, so the fallback costs only
debugger labels.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant