Enable Boehm parallel marking (PARALLEL_MARK) - #1818
Open
dg1sbg wants to merge 2 commits into
Open
Conversation
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
PARALLEL_MARKwas left undefined inboehm_config.h— the config-header template default — even though bdwgc's own CMake build defaults it ON andGC_THREADS/THREAD_LOCAL_ALLOCare already enabled here. Marking therefore ran single-threaded, andGC_mark_fromis 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:
ALL_INTERIOR_POINTERSis 1 (boehm_config.h:5) andGC_register_displacementis called forGENERAL_TAGandCONS_TAG(boehmGarbageCollection.cc:210,218-219).GC_new_kind,GC_new_proc,GC_MAKE_PROC,GC_DS_PROCandGC_make_descriptorhave zero occurrences outsidesrc/bdwgc/.gc_boot.cc:395-400setsglobal_lisp_kind,global_cons_kind,global_class_kind,global_container_kindandglobal_code_kindtoGC_I_NORMAL, andglobal_atomic_kindtoGC_I_PTRFREE.walk == precise_infobranch, where the per-stamp layout bitmaps are computed and validated (including theCons_Obitmap assertion) but never become GC mark descriptors.That the define reaches the collector
Worth stating explicitly, because
boehm_config.his included by only one Clasp source file andbuild.ninjacontains no bdwgc compile rules — which makes it look inert. It isn't:src/gctools/mygc.cis the single translation unit Clasp builds bdwgc from, and it doesConfirmed empirically both ways after the change:
GC_start_mark_threadsbecomes a defined symbol inlibclasp(it is absent before), and withGC_PRINT_STATS=1bdwgc reportsStarted 9 mark helper threads.Measured effect
Marking cost on a ~1 GB live heap, median of 12 full collections. Both sides are the same binary —
GC_MARKERS=1disables parallel marking at runtime — so there is no rebuild and no second variable:GC_MARKERS=1(serial)Roughly 3.4–3.75× faster marking, with non-overlapping ranges across all three runs.
Testing
macOS arm64,
:extensions (). Full regression suite andansi-testacross both GC variants and both build modes — the:bytecoderows are a real:build-mode :bytecodebuild (koga re-run, modules compiled:native nil,--baseresolving tobase.fasl), not merely a different image booted::native:native:bytecode:bytecodeEvery 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/:nativerun is test-by-test identical to the unmodified build. The bytecode image was additionally exercised under both GC variants from the:nativebuild 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.RANDOMis flaky for reasons unrelated to this PR (see #1816) and can reddenansi-testat random, so its exit code alone is not a verdict — compare the failure lists.