Marge Bot pushed to branch master at Glasgow Haskell Compiler / GHC
Commits:
-
46d4f963
by Sylvain Henry at 2026-07-29T06:38:40-04:00
17 changed files:
- + changelog.d/fix-heap-census-large-arrays-19048
- rts/Apply.cmm
- rts/ZeroSlop.c โ rts/MarkSlop.c
- rts/PrimOps.cmm
- rts/Printer.c
- rts/ProfHeap.c
- rts/RtsFlags.c
- rts/ThreadPaused.c
- rts/include/Cmm.h
- rts/include/rts/storage/ClosureMacros.h
- rts/rts.cabal
- rts/sm/NonMovingMark.c
- rts/sm/Sanity.c
- rts/sm/Storage.c
- + testsuite/tests/rts/T19048.hs
- + testsuite/tests/rts/T19048.stdout
- testsuite/tests/rts/all.T
Changes:
| 1 | +section: rts
|
|
| 2 | +synopsis: Correctly mark slop bytes when shrinking large arrays.
|
|
| 3 | + Heap census no longer traverses garbage-collected closures when profiling
|
|
| 4 | + is off.
|
|
| 5 | +issues: #19048
|
|
| 6 | +mrs: !15685 |
| ... | ... | @@ -736,8 +736,8 @@ for: |
| 736 | 736 | R1 = StgAP_STACK_fun(ap);
|
| 737 | 737 | |
| 738 | 738 | // Because of eager blackholing the closure no longer has correct size so
|
| 739 | - // threadPaused() can't correctly zero the slop, so we do it here. See #15571
|
|
| 740 | - // and Note [zeroing slop when overwriting closures].
|
|
| 739 | + // threadPaused() can't correctly mark the slop, so we do it here. See #15571
|
|
| 740 | + // and Note [marking slop when overwriting immutable closures].
|
|
| 741 | 741 | OVERWRITING_CLOSURE_SIZE(ap, BYTES_TO_WDS(SIZEOF_StgThunkHeader) + 2 + Words);
|
| 742 | 742 | |
| 743 | 743 | ENTER_R1();
|
| ... | ... | @@ -2,7 +2,7 @@ |
| 2 | 2 | *
|
| 3 | 3 | * (c) The GHC Team, 1998-2012
|
| 4 | 4 | *
|
| 5 | - * Utilities for zeroing slop callable from Cmm
|
|
| 5 | + * Utilities for marking slop callable from Cmm
|
|
| 6 | 6 | *
|
| 7 | 7 | * N.B. If you are in C you should rather using the inlineable utilities
|
| 8 | 8 | * (e.g. overwritingClosure) defined in ClosureMacros.h.
|
| ... | ... | @@ -11,14 +11,14 @@ |
| 11 | 11 | |
| 12 | 12 | #include "Rts.h"
|
| 13 | 13 | |
| 14 | -void stg_overwritingClosure (StgClosure *p)
|
|
| 14 | +void stg_writeSlopMarker (StgWord *slop, StgWord n)
|
|
| 15 | 15 | {
|
| 16 | - overwritingClosure(p);
|
|
| 16 | + writeSlopMarker(slop, n);
|
|
| 17 | 17 | }
|
| 18 | 18 | |
| 19 | -void stg_overwritingMutableClosureOfs (StgClosure *p, uint32_t offset)
|
|
| 19 | +void stg_overwritingClosure (StgClosure *p)
|
|
| 20 | 20 | {
|
| 21 | - overwritingMutableClosureOfs(p, offset);
|
|
| 21 | + overwritingClosure(p);
|
|
| 22 | 22 | }
|
| 23 | 23 | |
| 24 | 24 | void stg_overwritingClosureSize (StgClosure *p, uint32_t size /* in words */)
|
| ... | ... | @@ -216,10 +216,33 @@ stg_isMutableByteArrayWeaklyPinnedzh ( gcptr mba ) |
| 216 | 216 | * point if it's an older generation block, the mutator won't
|
| 217 | 217 | * allocate into those blocks anyway.
|
| 218 | 218 | *
|
| 219 | - * If check fails, fall back to the conservative code path: just zero the slop
|
|
| 219 | + * If check fails, fall back to the conservative code path: just mark the slop
|
|
| 220 | 220 | * and return when shrinking, or allocate a new array when growing.
|
| 221 | 221 | */
|
| 222 | 222 | |
| 223 | +/* Note [shrink-array slop marker]
|
|
| 224 | + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
| 225 | + * When shrinkSmallMutableArray# or shrinkMutableByteArray# creates n words of
|
|
| 226 | + * slop at address `slop_start`, we write an O(1) marker so that the heap
|
|
| 227 | + * census (heapCensus in ProfHeap.c) and the sanity checker (checkHeapChain in
|
|
| 228 | + * Sanity.c) can skip over the slop without reading stale heap pointers.
|
|
| 229 | + *
|
|
| 230 | + * The marker scheme (let n = number of slop words):
|
|
| 231 | + *
|
|
| 232 | + * n == 0 nothing written (no slop)
|
|
| 233 | + * n == 1 slop[0] = 0
|
|
| 234 | + * n >= 2 slop[0] = (StgWord)(-1) -- sentinel (0xFFFF...FFFF)
|
|
| 235 | + * slop[1] = n - 2 -- words remaining after the two-word header
|
|
| 236 | + *
|
|
| 237 | + * The sentinel value (StgWord)(-1) is safe because info-table pointers always
|
|
| 238 | + * live in the text segment; 0xFFFF...FFFF is never a valid info pointer.
|
|
| 239 | + *
|
|
| 240 | + * An array may be shrunk multiple times, leaving consecutive slop regions.
|
|
| 241 | + * Traversal code must therefore loop over all slop regions before advancing
|
|
| 242 | + * to the next live closure. See the while-loops in heapCensusBlock (ProfHeap.c)
|
|
| 243 | + * and checkHeapChain (Sanity.c).
|
|
| 244 | + */
|
|
| 245 | + |
|
| 223 | 246 | // shrink size of MutableByteArray in-place
|
| 224 | 247 | stg_shrinkMutableByteArrayzh ( gcptr mba, W_ new_size )
|
| 225 | 248 | // MutableByteArray# s -> Int# -> State# s -> State# s
|
| ... | ... | @@ -236,7 +259,11 @@ stg_shrinkMutableByteArrayzh ( gcptr mba, W_ new_size ) |
| 236 | 259 | bd = Bdescr(mba);
|
| 237 | 260 | if (bdescr_free(bd) != mba + WDS(old_wds) ||
|
| 238 | 261 | (bd != StgRegTable_rCurrentAlloc(BaseReg) && bd != Capability_pinned_object_block(MyCapability()))) {
|
| 239 | - OVERWRITING_CLOSURE_MUTABLE(mba, new_wds);
|
|
| 262 | + /* fall-back: not at end of nursery block; write slop marker.
|
|
| 263 | + See Note [shrink-array slop marker] */
|
|
| 264 | + W_ n;
|
|
| 265 | + n = old_wds - new_wds;
|
|
| 266 | + foreign "C" stg_writeSlopMarker(mba + WDS(new_wds) "ptr", n);
|
|
| 240 | 267 | StgArrBytes_bytes(mba) = new_size;
|
| 241 | 268 | // No need to call PROF_HEADER_CREATE. See Note [LDV profiling and resizing arrays]
|
| 242 | 269 | return ();
|
| ... | ... | @@ -321,8 +348,16 @@ again: |
| 321 | 348 | }
|
| 322 | 349 | }
|
| 323 | 350 | |
| 324 | - OVERWRITING_CLOSURE_MUTABLE(mba, (BYTES_TO_WDS(SIZEOF_StgSmallMutArrPtrs) +
|
|
| 325 | - new_size));
|
|
| 351 | + /* Write slop marker. See Note [shrink-array slop marker] */
|
|
| 352 | + W_ old_size_sma, n_sma, slop_start;
|
|
| 353 | + old_size_sma = StgSmallMutArrPtrs_ptrs(mba);
|
|
| 354 | + n_sma = old_size_sma - new_size;
|
|
| 355 | + slop_start = mba + SIZEOF_StgSmallMutArrPtrs + WDS(new_size);
|
|
| 356 | + foreign "C" stg_writeSlopMarker(slop_start "ptr", n_sma);
|
|
| 357 | + |
|
| 358 | + // No ordering needed: the concurrent mark thread bounds the slop with the
|
|
| 359 | + // marker's own release/acquire (Note [Slop marker memory ordering]), not
|
|
| 360 | + // with this store, and reading the new size it never scans the slop.
|
|
| 326 | 361 | StgSmallMutArrPtrs_ptrs(mba) = new_size;
|
| 327 | 362 | // No need to call PROF_HEADER_CREATE. See Note [LDV profiling and resizing arrays]
|
| 328 | 363 |
| ... | ... | @@ -1001,8 +1001,19 @@ findPtrBlocks (StgPtr p, bdescr *bd, StgPtr arr[], int arr_size, int i) |
| 1001 | 1001 | if (UNTAG_CONST_CLOSURE((StgClosure*)*q) == (const StgClosure *)p) {
|
| 1002 | 1002 | if (i < arr_size) {
|
| 1003 | 1003 | for (r = bd->start; r < bd->free; r = end) {
|
| 1004 | - // skip over zeroed-out slop
|
|
| 1005 | - while (*r == 0) r++;
|
|
| 1004 | + // skip over marked slop; loop because an array
|
|
| 1005 | + // may have been shrunk multiple times.
|
|
| 1006 | + // See Note [shrink-array slop marker] in PrimOps.cmm.
|
|
| 1007 | + while (r < bd->free) {
|
|
| 1008 | + if (!*r) {
|
|
| 1009 | + r++;
|
|
| 1010 | + } else if (*r == (StgWord)(-1)) {
|
|
| 1011 | + StgWord skip = *(r + 1);
|
|
| 1012 | + r += 2 + skip;
|
|
| 1013 | + } else {
|
|
| 1014 | + break;
|
|
| 1015 | + }
|
|
| 1016 | + }
|
|
| 1006 | 1017 | if (!LOOKS_LIKE_CLOSURE_PTR(r)) {
|
| 1007 | 1018 | debugBelch("%p found at %p, no closure at %p\n",
|
| 1008 | 1019 | p, q, r);
|
| ... | ... | @@ -1317,20 +1317,37 @@ heapCensusBlock(Census *census, bdescr *bd) |
| 1317 | 1317 | |
| 1318 | 1318 | p += size;
|
| 1319 | 1319 | |
| 1320 | - /* skip over slop, see Note [slop on the heap] */
|
|
| 1321 | - while (p < bd->free && !*p) p++;
|
|
| 1322 | - /* Note [skipping slop in the heap profiler]
|
|
| 1323 | - * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
| 1324 | - * We make sure to zero slop that can remain after a major GC so
|
|
| 1325 | - * here we can assume any slop words we see until the block's free
|
|
| 1326 | - * pointer are zero. Since info pointers are always nonzero we can
|
|
| 1327 | - * use this to scan for the next valid heap closure.
|
|
| 1328 | - *
|
|
| 1329 | - * Note that not all types of slop are relevant here, only the ones
|
|
| 1330 | - * that can remain after major GC. So essentially just large objects
|
|
| 1331 | - * and pinned objects. All other closures will have been packed nice
|
|
| 1332 | - * and tight into fresh blocks.
|
|
| 1333 | - */
|
|
| 1320 | + /* skip over slop (zero words from large/pinned objects, or
|
|
| 1321 | + shrink-array slop markers); loop because an array may have been
|
|
| 1322 | + shrunk multiple times, leaving consecutive slop regions.
|
|
| 1323 | + See Note [slop on the heap] and Note [shrink-array slop marker]
|
|
| 1324 | + in PrimOps.cmm.
|
|
| 1325 | + |
|
| 1326 | + Note [skipping slop in the heap profiler]
|
|
| 1327 | + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
| 1328 | + Slop left behind after major GC comes in two forms:
|
|
| 1329 | + |
|
| 1330 | + 1. Zero words: alignment padding for large/pinned objects.
|
|
| 1331 | + We zero these explicitly (see MEMSET_SLOP_W in allocatePinned).
|
|
| 1332 | + |
|
| 1333 | + 2. Shrink-array slop markers: written by stg_shrinkMutableByteArrayzh
|
|
| 1334 | + and stg_shrinkSmallMutableArrayzh in all build modes. A single-word
|
|
| 1335 | + slop region is represented as a zero word; a multi-word region begins
|
|
| 1336 | + with the sentinel (StgWord)(-1) followed by a count of additional
|
|
| 1337 | + words. See Note [shrink-array slop marker] in PrimOps.cmm.
|
|
| 1338 | + |
|
| 1339 | + Because an array can be shrunk multiple times, we loop until we
|
|
| 1340 | + see a word that looks like a valid info pointer. */
|
|
| 1341 | + while (p < bd->free) {
|
|
| 1342 | + if (!*p) {
|
|
| 1343 | + p++;
|
|
| 1344 | + } else if (*p == (StgWord)(-1)) {
|
|
| 1345 | + StgWord skip = *(p + 1);
|
|
| 1346 | + p += 2 + skip;
|
|
| 1347 | + } else {
|
|
| 1348 | + break;
|
|
| 1349 | + }
|
|
| 1350 | + }
|
|
| 1334 | 1351 | }
|
| 1335 | 1352 | }
|
| 1336 | 1353 |
| ... | ... | @@ -2026,7 +2026,7 @@ static void normaliseRtsOpts (void) |
| 2026 | 2026 | |
| 2027 | 2027 | #if !defined(PROFILING) && !defined(DEBUG)
|
| 2028 | 2028 | // The mark-region collector is incompatible with heap census unless
|
| 2029 | - // we zero slop of blackhole'd thunks, which doesn't happen in the
|
|
| 2029 | + // we mark slop of blackhole'd thunks, which doesn't happen in the
|
|
| 2030 | 2030 | // vanilla way. See #9666.
|
| 2031 | 2031 | if (RtsFlags.ProfFlags.doHeapProfile && RtsFlags.GcFlags.sweep) {
|
| 2032 | 2032 | barf("The mark-region collector can only be used with profiling\n"
|
| ... | ... | @@ -383,7 +383,7 @@ threadPaused(Capability *cap, StgTSO *tso) |
| 383 | 383 | }
|
| 384 | 384 | }
|
| 385 | 385 | |
| 386 | - // zero out the slop so that the sanity checker can tell
|
|
| 386 | + // mark the slop so that the sanity checker can tell
|
|
| 387 | 387 | // where the next closure is. N.B. We mustn't do this until we have
|
| 388 | 388 | // pushed the free variables to the update remembered set above.
|
| 389 | 389 | OVERWRITING_CLOSURE_SIZE(bh, closure_sizeW_(bh, INFO_PTR_TO_STRUCT(bh_info)));
|
| ... | ... | @@ -659,15 +659,9 @@ |
| 659 | 659 | #if defined(PROFILING) || defined(DEBUG)
|
| 660 | 660 | #define OVERWRITING_CLOSURE_SIZE(c, size) foreign "C" stg_overwritingClosureSize(c "ptr", size)
|
| 661 | 661 | #define OVERWRITING_CLOSURE(c) foreign "C" stg_overwritingClosure(c "ptr")
|
| 662 | -#define OVERWRITING_CLOSURE_MUTABLE(c, off) foreign "C" stg_overwritingMutableClosureOfs(c "ptr", off)
|
|
| 663 | 662 | #else
|
| 664 | 663 | #define OVERWRITING_CLOSURE_SIZE(c, size) /* nothing */
|
| 665 | 664 | #define OVERWRITING_CLOSURE(c) /* nothing */
|
| 666 | -/* This is used to zero slop after shrunk arrays. It is important that we do
|
|
| 667 | - * this whenever profiling is enabled as described in Note [slop on the heap]
|
|
| 668 | - * in Storage.c. */
|
|
| 669 | -#define OVERWRITING_CLOSURE_MUTABLE(c, off) \
|
|
| 670 | - if (TO_W_(RtsFlags_ProfFlags_doHeapProfile(RtsFlags)) != 0) { foreign "C" stg_overwritingMutableClosureOfs(c "ptr", off); }
|
|
| 671 | 665 | #endif
|
| 672 | 666 | |
| 673 | 667 | #define IS_STACK_CLEAN(stack) \
|
| ... | ... | @@ -533,29 +533,34 @@ EXTERN_INLINE StgWord8 *mutArrPtrsCard (StgMutArrPtrs *a, W_ n) |
| 533 | 533 | */
|
| 534 | 534 | |
| 535 | 535 | /*
|
| 536 | - Note [zeroing slop when overwriting closures]
|
|
| 537 | - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
| 538 | - When we overwrite a closure in the heap with a smaller one, in some scenarios
|
|
| 539 | - we need to write zero words into "slop"; the memory that is left
|
|
| 540 | - unoccupied. See Note [slop on the heap]
|
|
| 536 | + Note [marking slop when overwriting immutable closures]
|
|
| 537 | + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
| 538 | + When we overwrite a closure in the heap with a smaller one, we need to mark
|
|
| 539 | + the "slop" -- the memory that is left unoccupied -- so that the heap can be
|
|
| 540 | + linearly scanned. See Note [slop on the heap]
|
|
| 541 | 541 | |
| 542 | - Zeroing slop is required for:
|
|
| 542 | + For mutable closures (e.g. shrinking arrays), slop is always marked
|
|
| 543 | + unconditionally via writeSlopMarker, in all build modes.
|
|
| 544 | + See Note [shrink-array slop marker] in PrimOps.cmm.
|
|
| 545 | + |
|
| 546 | + For immutable closures (e.g. thunks overwritten with indirections), slop
|
|
| 547 | + marking is only needed for:
|
|
| 543 | 548 | |
| 544 | 549 | - full-heap sanity checks (DEBUG, and +RTS -DS),
|
| 545 | 550 | |
| 546 | - - LDV profiling (PROFILING, and +RTS -hb) and
|
|
| 551 | + - LDV profiling (PROFILING, and +RTS -hb)
|
|
| 547 | 552 | |
| 548 | - However we can get into trouble if we're zeroing slop for ordinarily
|
|
| 549 | - immutable closures when using multiple threads, since there is nothing
|
|
| 550 | - preventing another thread from still being in the process of reading the
|
|
| 551 | - memory we're about to zero.
|
|
| 553 | + However we can get into trouble if we're marking slop for immutable closures
|
|
| 554 | + when using multiple threads, since there is nothing preventing another thread
|
|
| 555 | + from still being in the process of reading the memory we're about to
|
|
| 556 | + overwrite.
|
|
| 552 | 557 | |
| 553 | - Thus, with the THREADED RTS and +RTS -N2 or greater we must not zero
|
|
| 558 | + Thus, with the THREADED RTS and +RTS -N2 or greater we must not mark
|
|
| 554 | 559 | immutable closure's slop. Similarly, the concurrent GC's mark thread
|
| 555 | - may race when a mutator during slop-zeroing. Consequently, we also disable
|
|
| 556 | - zeroing when the non-moving GC is in use.
|
|
| 560 | + may race with a mutator during slop marking. Consequently, we also disable
|
|
| 561 | + marking of immutable closures when the non-moving GC is in use.
|
|
| 557 | 562 | |
| 558 | - Hence, an immutable closure's slop is zeroed when either:
|
|
| 563 | + Hence, an immutable closure's slop is marked when either:
|
|
| 559 | 564 | |
| 560 | 565 | - PROFILING && era > 0 (LDV is on) && !nonmoving-gc-enabled or
|
| 561 | 566 | - !THREADED && DEBUG
|
| ... | ... | @@ -575,15 +580,11 @@ EXTERN_INLINE StgWord8 *mutArrPtrsCard (StgMutArrPtrs *a, W_ n) |
| 575 | 580 | overwritingClosure(c)
|
| 576 | 581 | #define OVERWRITING_CLOSURE_SIZE(c, size) \
|
| 577 | 582 | overwritingClosureSize(c, size)
|
| 578 | -#define OVERWRITING_CLOSURE_MUTABLE(c, off) \
|
|
| 579 | - overwritingMutableClosureOfs(c, off)
|
|
| 580 | 583 | #else
|
| 581 | 584 | #define OVERWRITING_CLOSURE(c) \
|
| 582 | 585 | do { (void) sizeof(c); } while(0)
|
| 583 | 586 | #define OVERWRITING_CLOSURE_SIZE(c, size) \
|
| 584 | 587 | do { (void) sizeof(c); (void) sizeof(size); } while(0)
|
| 585 | -#define OVERWRITING_CLOSURE_MUTABLE(c, off) \
|
|
| 586 | - do { (void) sizeof(c); (void) sizeof(off); } while(0)
|
|
| 587 | 588 | #endif
|
| 588 | 589 | |
| 589 | 590 | #if defined(PROFILING)
|
| ... | ... | @@ -591,16 +592,57 @@ void LDV_recordDead (const StgClosure *c, uint32_t size); |
| 591 | 592 | RTS_PRIVATE bool isInherentlyUsed ( StgHalfWord closure_type );
|
| 592 | 593 | #endif
|
| 593 | 594 | |
| 595 | +// Note [Slop marker memory ordering]
|
|
| 596 | +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
| 597 | +// The non-moving GC mark thread reads SmallMutArrPtrs payload elements
|
|
| 598 | +// concurrently with the mutator, which may shrink the array via
|
|
| 599 | +// stg_shrinkSmallMutableArrayzh. Shrinking writes a slop marker over the
|
|
| 600 | +// vacated elements (see Note [shrink-array slop marker] in PrimOps.cmm):
|
|
| 601 | +//
|
|
| 602 | +// n == 1: slop[0] = 0
|
|
| 603 | +// n >= 2: slop[0] = -1 (sentinel)
|
|
| 604 | +// slop[1] = n-2 (count of further slop words)
|
|
| 605 | +//
|
|
| 606 | +// The mark thread reads elements with ACQUIRE_LOAD and, at each position i>0,
|
|
| 607 | +// re-reads element i-1 after reading i to detect a concurrently written -1
|
|
| 608 | +// sentinel (see rts/sm/NonMovingMark.c). For this check to be sound the
|
|
| 609 | +// store of -1 to slop[0] must be visible to the reader when it observes the
|
|
| 610 | +// skip count at slop[1]. This is guaranteed by the release-acquire pairing:
|
|
| 611 | +// the RELEASE_STORE of n-2 to slop[1] ensures that the prior RELAXED_STORE
|
|
| 612 | +// of -1 to slop[0] is visible to any thread that performs an ACQUIRE_LOAD of
|
|
| 613 | +// slop[1] and sees the skip count.
|
|
| 614 | +//
|
|
| 615 | +// The re-read of element i-1 is only performed when the value c just read at
|
|
| 616 | +// position i could plausibly be a skip count, i.e. when (StgWord)c < n-i
|
|
| 617 | +// (a skip count at position i must satisfy i + skip <= n-1, so skip < n-i).
|
|
| 618 | +// Values at or above that bound are valid closure pointers, so no re-read is
|
|
| 619 | +// needed. In practice closure pointers are word-aligned kernel addresses and
|
|
| 620 | +// far exceed any plausible skip count, so this eliminates virtually all of
|
|
| 621 | +// the redundant re-reads.
|
|
| 622 | +//
|
|
| 623 | +// The non-moving GC only runs in the threaded RTS, where RELEASE_STORE and
|
|
| 624 | +// ACQUIRE_LOAD are both __atomic_* operations. The _ALWAYS variants are not
|
|
| 625 | +// needed here.
|
|
| 626 | +INLINE_HEADER void writeSlopMarker(StgWord *slop, StgWord n)
|
|
| 627 | +{
|
|
| 628 | + // See Note [Slop marker memory ordering]
|
|
| 629 | + if (n == 1) {
|
|
| 630 | + RELAXED_STORE(&slop[0], (StgWord)0);
|
|
| 631 | + } else if (n >= 2) {
|
|
| 632 | + RELAXED_STORE(&slop[0], (StgWord)(-1));
|
|
| 633 | + RELEASE_STORE(&slop[1], n - 2);
|
|
| 634 | + }
|
|
| 635 | +}
|
|
| 636 | + |
|
| 594 | 637 | INLINE_HEADER void
|
| 595 | -zeroSlop (StgClosure *p,
|
|
| 596 | - uint32_t offset, /*< offset to start zeroing at, in words */
|
|
| 597 | - uint32_t size, /*< total closure size, in words */
|
|
| 598 | - bool known_mutable /*< is this a closure who's slop we can always zero? */
|
|
| 638 | +markImmutableSlop (StgClosure *p,
|
|
| 639 | + uint32_t offset, /*< offset to start marking at, in words */
|
|
| 640 | + uint32_t size /*< total closure size, in words */
|
|
| 599 | 641 | )
|
| 600 | 642 | {
|
| 601 | - // see Note [zeroing slop when overwriting closures], also #8402
|
|
| 643 | + // see Note [marking slop when overwriting immutable closures], also #8402
|
|
| 602 | 644 | |
| 603 | - const bool want_to_zero_immutable_slop = false
|
|
| 645 | + const bool want_to_mark = false
|
|
| 604 | 646 | // Sanity checking (-DS) is enabled
|
| 605 | 647 | || RTS_DEREF(RtsFlags).DebugFlags.sanity
|
| 606 | 648 | #if defined(PROFILING)
|
| ... | ... | @@ -609,44 +651,23 @@ zeroSlop (StgClosure *p, |
| 609 | 651 | #endif
|
| 610 | 652 | ;
|
| 611 | 653 | |
| 612 | - const bool can_zero_immutable_slop =
|
|
| 654 | + const bool can_mark =
|
|
| 613 | 655 | // Only if we're running single threaded.
|
| 614 | 656 | getNumCapabilities() == 1
|
| 615 | 657 | && !RTS_DEREF(RtsFlags).GcFlags.useNonmoving; // see #23170
|
| 616 | 658 | |
| 617 | - const bool zero_slop_immutable =
|
|
| 618 | - want_to_zero_immutable_slop && can_zero_immutable_slop;
|
|
| 619 | - |
|
| 620 | - const bool zero_slop_mutable =
|
|
| 621 | -#if defined(PROFILING)
|
|
| 622 | - // Always zero mutable closure slop when profiling. We do this to cover
|
|
| 623 | - // the case of shrinking mutable arrays in pinned blocks for the heap
|
|
| 624 | - // profiler, see Note [skipping slop in the heap profiler]
|
|
| 625 | - //
|
|
| 626 | - // TODO: We could make this check more specific and only zero if the
|
|
| 627 | - // object is in a BF_PINNED bdescr here. Update Note [slop on the heap]
|
|
| 628 | - // and [zeroing slop when overwriting closures] if you change this.
|
|
| 629 | - true
|
|
| 630 | -#else
|
|
| 631 | - zero_slop_immutable
|
|
| 632 | -#endif
|
|
| 633 | - ;
|
|
| 634 | - |
|
| 635 | - const bool zero_slop =
|
|
| 636 | - // If we're not sure this is a mutable closure treat it like an
|
|
| 637 | - // immutable one.
|
|
| 638 | - known_mutable ? zero_slop_mutable : zero_slop_immutable;
|
|
| 639 | - |
|
| 640 | - if(!zero_slop)
|
|
| 659 | + if(!(want_to_mark && can_mark))
|
|
| 641 | 660 | return;
|
| 642 | 661 | |
| 643 | - for (uint32_t i = offset; i < size; i++) {
|
|
| 644 | - ((StgWord *)p)[i] = 0;
|
|
| 645 | - }
|
|
| 662 | + // Write a slop marker so that the heap profiler and sanity checker can skip
|
|
| 663 | + // over the slop without reading stale heap pointers.
|
|
| 664 | + // See Note [shrink-array slop marker] in PrimOps.cmm for the encoding.
|
|
| 665 | + writeSlopMarker((StgWord *)p + offset, size - offset);
|
|
| 646 | 666 | }
|
| 647 | 667 | |
| 648 | 668 | // N.B. the stg_* variants of the utilities below are only for calling from
|
| 649 | 669 | // Cmm. The INLINE_HEADER functions should be used when in C.
|
| 670 | +void stg_writeSlopMarker (StgWord *slop, StgWord n);
|
|
| 650 | 671 | void stg_overwritingClosure (StgClosure *p);
|
| 651 | 672 | INLINE_HEADER void overwritingClosure (StgClosure *p)
|
| 652 | 673 | {
|
| ... | ... | @@ -655,31 +676,10 @@ INLINE_HEADER void overwritingClosure (StgClosure *p) |
| 655 | 676 | if(era > 0 && !isInherentlyUsed(get_itbl(p)->type))
|
| 656 | 677 | LDV_recordDead(p, size);
|
| 657 | 678 | #endif
|
| 658 | - zeroSlop(p, sizeofW(StgThunkHeader), size, /*known_mutable=*/false);
|
|
| 679 | + markImmutableSlop(p, sizeofW(StgThunkHeader), size);
|
|
| 659 | 680 | }
|
| 660 | 681 | |
| 661 | 682 | |
| 662 | -// Version of 'overwritingClosure' which overwrites only a suffix of a
|
|
| 663 | -// closure. The offset is expressed in words relative to 'p' and shall
|
|
| 664 | -// be less than or equal to closure_sizeW(p), and usually at least as
|
|
| 665 | -// large as the respective thunk header.
|
|
| 666 | -void stg_overwritingMutableClosureOfs (StgClosure *p, uint32_t offset);
|
|
| 667 | -INLINE_HEADER void overwritingMutableClosureOfs (StgClosure *p, uint32_t offset)
|
|
| 668 | -{
|
|
| 669 | - // Since overwritingClosureOfs is only ever called by:
|
|
| 670 | - //
|
|
| 671 | - // - shrinkMutableByteArray# (ARR_WORDS) and
|
|
| 672 | - //
|
|
| 673 | - // - shrinkSmallMutableArray# (SMALL_MUT_ARR_PTRS)
|
|
| 674 | - //
|
|
| 675 | - // we can safely omit the Ldv_recordDead call. Since these closures are
|
|
| 676 | - // considered inherently used we don't need to track their destruction.
|
|
| 677 | -#if defined(PROFILING)
|
|
| 678 | - ASSERT(isInherentlyUsed(get_itbl(p)->type) == true);
|
|
| 679 | -#endif
|
|
| 680 | - zeroSlop(p, offset, closure_sizeW(p), /*known_mutable=*/true);
|
|
| 681 | -}
|
|
| 682 | - |
|
| 683 | 683 | // Version of 'overwritingClosure' which takes closure size as argument.
|
| 684 | 684 | void stg_overwritingClosureSize (StgClosure *p, uint32_t size /* in words */);
|
| 685 | 685 | INLINE_HEADER void overwritingClosureSize (StgClosure *p, uint32_t size)
|
| ... | ... | @@ -691,5 +691,5 @@ INLINE_HEADER void overwritingClosureSize (StgClosure *p, uint32_t size) |
| 691 | 691 | if(era > 0)
|
| 692 | 692 | LDV_recordDead(p, size);
|
| 693 | 693 | #endif
|
| 694 | - zeroSlop(p, sizeofW(StgThunkHeader), size, /*known_mutable=*/false);
|
|
| 694 | + markImmutableSlop(p, sizeofW(StgThunkHeader), size);
|
|
| 695 | 695 | } |
| ... | ... | @@ -473,7 +473,7 @@ library |
| 473 | 473 | TSANUtils.c
|
| 474 | 474 | WSDeque.c
|
| 475 | 475 | Weak.c
|
| 476 | - ZeroSlop.c
|
|
| 476 | + MarkSlop.c
|
|
| 477 | 477 | eventlog/EventLog.c
|
| 478 | 478 | eventlog/EventLogWriter.c
|
| 479 | 479 | hooks/FlagDefaults.c
|
| ... | ... | @@ -1663,9 +1663,42 @@ mark_closure (MarkQueue *queue, const StgClosure *p0, StgClosure **origin) |
| 1663 | 1663 | case SMALL_MUT_ARR_PTRS_FROZEN_CLEAN:
|
| 1664 | 1664 | case SMALL_MUT_ARR_PTRS_FROZEN_DIRTY: {
|
| 1665 | 1665 | StgSmallMutArrPtrs *arr = (StgSmallMutArrPtrs *) p;
|
| 1666 | - for (StgWord i = 0; i < arr->ptrs; i++) {
|
|
| 1667 | - StgClosure **field = &arr->payload[i];
|
|
| 1668 | - markQueuePushClosure(queue, ACQUIRE_LOAD(field), field);
|
|
| 1666 | + StgWord n = arr->ptrs;
|
|
| 1667 | + if (n == 0) break;
|
|
| 1668 | + |
|
| 1669 | + for (StgWord i = 0; i < n; i++) {
|
|
| 1670 | + StgClosure *c = ACQUIRE_LOAD(&arr->payload[i]);
|
|
| 1671 | + // If NULL or -1, we know the rest is slop
|
|
| 1672 | + if (c == NULL || c == (StgClosure *)(-1)) break;
|
|
| 1673 | + // A valid skip count at position i must satisfy
|
|
| 1674 | + // i + skip <= n-1 (sentinel at i-1, count at i, skip more words)
|
|
| 1675 | + // i.e. skip < n-i. If c is out of that range it cannot be a skip
|
|
| 1676 | + // count, so we must have read a valid closure pointer.
|
|
| 1677 | + bool maybe_slop_count = (StgWord)c < n - i;
|
|
| 1678 | + if (maybe_slop_count && i != 0) {
|
|
| 1679 | + // Otherwise re-read the previous element: the mutator may have
|
|
| 1680 | + // written -1 there after we last saw it, making the current
|
|
| 1681 | + // word the skip count rather than a valid closure pointer.
|
|
| 1682 | + //
|
|
| 1683 | + // The ACQUIRE_LOAD of payload[i] above synchronizes with the
|
|
| 1684 | + // RELEASE_STORE in writeSlopMarker, so a RELAXED_LOAD suffices
|
|
| 1685 | + // here; see Note [Slop marker memory ordering] in
|
|
| 1686 | + // rts/include/rts/storage/ClosureMacros.h.
|
|
| 1687 | + if (RELAXED_LOAD(&arr->payload[i-1]) == (StgClosure *)(-1)) break;
|
|
| 1688 | + }
|
|
| 1689 | + |
|
| 1690 | + // Track origin so indirections reached through array elements get
|
|
| 1691 | + // short-cut (see Note [Origin references in the nonmoving
|
|
| 1692 | + // collector] in NonMovingMark.h), but only when c cannot be a skip
|
|
| 1693 | + // count, i.e. c >= n-i.
|
|
| 1694 | + // The collapse rewrites the cell with a CAS that fires only if it
|
|
| 1695 | + // still holds c; restricting to c >= n-i guarantees c differs from
|
|
| 1696 | + // any skip count the mutator could write at this cell while
|
|
| 1697 | + // concurrently shrinking the array, so the CAS can never clobber a
|
|
| 1698 | + // slop marker. Real heap addresses are far above n, so in practice
|
|
| 1699 | + // every element is still short-cut.
|
|
| 1700 | + StgClosure **origin = maybe_slop_count ? NULL : &arr->payload[i];
|
|
| 1701 | + markQueuePushClosure(queue, c, origin);
|
|
| 1669 | 1702 | }
|
| 1670 | 1703 | break;
|
| 1671 | 1704 | }
|
| ... | ... | @@ -605,9 +605,19 @@ void checkHeapChain (bdescr *bd) |
| 605 | 605 | ASSERT( size >= MIN_PAYLOAD_SIZE + sizeofW(StgHeader) );
|
| 606 | 606 | p += size;
|
| 607 | 607 | |
| 608 | - /* skip over slop, see Note [slop on the heap] */
|
|
| 609 | - while (p < bd->free &&
|
|
| 610 | - (*p < 0x1000 || !LOOKS_LIKE_INFO_PTR(*p))) { p++; }
|
|
| 608 | + /* skip slop; loop because an array may have been shrunk
|
|
| 609 | + multiple times. See Note [slop on the heap] in Storage.c
|
|
| 610 | + and Note [shrink-array slop marker] in PrimOps.cmm. */
|
|
| 611 | + while (p < bd->free) {
|
|
| 612 | + if (!*p) {
|
|
| 613 | + p++;
|
|
| 614 | + } else if (*p == (StgWord)(-1)) {
|
|
| 615 | + StgWord skip = *(p + 1);
|
|
| 616 | + p += 2 + skip;
|
|
| 617 | + } else {
|
|
| 618 | + break;
|
|
| 619 | + }
|
|
| 620 | + }
|
|
| 611 | 621 | }
|
| 612 | 622 | }
|
| 613 | 623 | }
|
| ... | ... | @@ -1013,9 +1023,9 @@ static void checkGeneration (generation *gen, |
| 1013 | 1023 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
| 1014 | 1024 | // heap sanity checking doesn't work with SMP for two reasons:
|
| 1015 | 1025 | //
|
| 1016 | - // * We can't zero the slop. However, we can sanity-check the heap after a
|
|
| 1026 | + // * We can't mark the slop. However, we can sanity-check the heap after a
|
|
| 1017 | 1027 | // major gc, because there is no slop. See also Updates.h and Note
|
| 1018 | - // [zeroing slop when overwriting closures].
|
|
| 1028 | + // [marking slop when overwriting immutable closures].
|
|
| 1019 | 1029 | //
|
| 1020 | 1030 | // * The nonmoving collector may be mutating its large object lists,
|
| 1021 | 1031 | // unless we were in fact called by the nonmoving collector.
|
| ... | ... | @@ -1033,29 +1033,30 @@ accountAllocation(Capability *cap, W_ n) |
| 1033 | 1033 | *
|
| 1034 | 1034 | * During GC the RTS overwrites closures with forwarding pointers, this can
|
| 1035 | 1035 | * leave slop behind depending on the size of the closure being
|
| 1036 | - * overwritten. See Note [zeroing slop when overwriting closures].
|
|
| 1036 | + * overwritten. See Note [marking slop when overwriting immutable closures].
|
|
| 1037 | 1037 | *
|
| 1038 | - * Under various ways we actually zero slop so we can linearly scan over blocks
|
|
| 1039 | - * of closures. This trick is used by the sanity checking code and the heap
|
|
| 1040 | - * profiler, see Note [skipping slop in the heap profiler].
|
|
| 1038 | + * To allow the heap profiler and sanity checker to linearly scan over heap
|
|
| 1039 | + * blocks, slop must be identifiable without reading stale heap pointers.
|
|
| 1040 | + * See Note [skipping slop in the heap profiler]
|
|
| 1041 | 1041 | *
|
| 1042 | - * In general we zero:
|
|
| 1042 | + * Shrunk-array slop has a further, concurrent reader: the non-moving GC mark
|
|
| 1043 | + * thread scans SmallMutArrPtrs payloads while the mutator may be shrinking
|
|
| 1044 | + * them, so it must identify the slop with the right memory ordering. See Note
|
|
| 1045 | + * [Slop marker memory ordering] in ClosureMacros.h.
|
|
| 1043 | 1046 | *
|
| 1047 | + * For pinned/large-object alignment slop we use explicit zeroing:
|
|
| 1044 | 1048 | * - Pinned object alignment slop, see MEMSET_SLOP_W in allocatePinned.
|
| 1045 | 1049 | * - Large object alignment slop, see MEMSET_SLOP_W in allocatePinned.
|
| 1046 | - * - Shrunk array slop, see OVERWRITING_CLOSURE_MUTABLE.
|
|
| 1047 | 1050 | *
|
| 1048 | - * Note that this is necessary even in the vanilla (e.g. non-profiling) RTS
|
|
| 1049 | - * since the user may trigger a heap census via +RTS -hT, which can be used
|
|
| 1050 | - * even when not linking against the profiled RTS. Failing to zero slop
|
|
| 1051 | - * due to array shrinking has resulted in a few nasty bugs (#17572, #9666).
|
|
| 1052 | - * However, since array shrink may result in large amounts of slop (unlike
|
|
| 1053 | - * alignment), we take care to only zero such slop when heap profiling or DEBUG
|
|
| 1054 | - * are enabled.
|
|
| 1051 | + * For shrunk-array slop we write an O(1) marker in all build modes.
|
|
| 1052 | + * See Note [shrink-array slop marker] in PrimOps.cmm for the encoding.
|
|
| 1053 | + * This replaces the old approach of zeroing the entire slop region, which was a
|
|
| 1054 | + * no-op in vanilla (non-profiling, non-debug) builds and caused heap-census
|
|
| 1055 | + * crashes (#19048, #17572, #9666).
|
|
| 1055 | 1056 | *
|
| 1056 | - * When performing LDV profiling or using a (single threaded) debug RTS we zero
|
|
| 1057 | - * slop even when overwriting immutable closures, see Note [zeroing slop when
|
|
| 1058 | - * overwriting closures].
|
|
| 1057 | + * When performing LDV profiling or using a (single threaded) debug RTS we mark
|
|
| 1058 | + * slop even when overwriting immutable closures, see Note [marking slop when
|
|
| 1059 | + * overwriting immutable closures].
|
|
| 1059 | 1060 | */
|
| 1060 | 1061 | |
| 1061 | 1062 | /*
|
| 1 | +{-# LANGUAGE MagicHash, UnboxedTuples, BlockArguments #-}
|
|
| 2 | +module Main where
|
|
| 3 | + |
|
| 4 | +import GHC.Exts
|
|
| 5 | +import GHC.ST (ST(..), runST)
|
|
| 6 | +import System.Mem (performMajorGC)
|
|
| 7 | + |
|
| 8 | +-- Lifted wrapper so SmallArray# can appear in non-unlifted positions.
|
|
| 9 | +data SmallArr a = SmallArr (SmallArray# a)
|
|
| 10 | + |
|
| 11 | +main :: IO ()
|
|
| 12 | +main = do
|
|
| 13 | + let arr = buildArr
|
|
| 14 | + let n = case arr of SmallArr a -> I# (sizeofSmallArray# a)
|
|
| 15 | + putStrLn $ "size after shrink = " ++ show n
|
|
| 16 | + -- With +RTS -hT -i0 this triggers heapCensus.
|
|
| 17 | + -- The census advances past the 10 live elements, then hits the 490
|
|
| 18 | + -- stale heap pointers in the slop and crashes.
|
|
| 19 | + performMajorGC
|
|
| 20 | + -- Keep arr alive across the GC by reading from it after.
|
|
| 21 | + let v = case arr of SmallArr a -> case indexSmallArray# a 0# of (# x #) -> x
|
|
| 22 | + putStrLn $ "arr[0] = " ++ show (v :: Integer)
|
|
| 23 | + putStrLn "survived"
|
|
| 24 | + |
|
| 25 | +-- Allocate 500 slots, write a DISTINCT Integer to every slot, then
|
|
| 26 | +-- shrink to 10. Slots [10..499] become slop: they still hold live,
|
|
| 27 | +-- non-zero heap pointers in the raw memory, but the ptrs header field
|
|
| 28 | +-- says there are only 10 elements. zeroSlop is a no-op in non-PROFILING
|
|
| 29 | +-- builds (even when -hT is active), so the slop is never cleared.
|
|
| 30 | +buildArr :: SmallArr Integer
|
|
| 31 | +buildArr = runST $ ST \s0 ->
|
|
| 32 | + -- All 500 slots start with a non-null initial value.
|
|
| 33 | + case newSmallArray# 500# (0 :: Integer) s0 of { (# s1, ma #) ->
|
|
| 34 | + -- Overwrite every slot with a distinct Integer so each holds a
|
|
| 35 | + -- unique heap pointer (no sharing, definitely non-zero).
|
|
| 36 | + case fill 499 ma s1 of { s2 ->
|
|
| 37 | + -- Shrink: ptrs = 10, but physical slots [10..499] are NOT zeroed.
|
|
| 38 | + case shrinkSmallMutableArray# ma 10# s2 of { s3 ->
|
|
| 39 | + case unsafeFreezeSmallArray# ma s3 of { (# s4, a #) ->
|
|
| 40 | + (# s4, SmallArr a #) }}}}
|
|
| 41 | + |
|
| 42 | +-- Fill slots [0..n] each with a distinct Integer value (n, n-1, ..., 0).
|
|
| 43 | +-- 'Integer' guarantees a genuine heap object for every value.
|
|
| 44 | +fill :: Int -> SmallMutableArray# s Integer -> State# s -> State# s
|
|
| 45 | +fill 0 ma s = writeSmallArray# ma 0# (0 :: Integer) s
|
|
| 46 | +fill n ma s =
|
|
| 47 | + let I# n# = n
|
|
| 48 | + in case writeSmallArray# ma n# (fromIntegral n :: Integer) s of
|
|
| 49 | + s' -> fill (n - 1) ma s' |
| 1 | +size after shrink = 10
|
|
| 2 | +arr[0] = 0
|
|
| 3 | +survived |
| ... | ... | @@ -702,3 +702,11 @@ test('T27123', [when(have_profiling(), extra_ways(['prof']))], compile_and_run, |
| 702 | 702 | test('T27434',
|
| 703 | 703 | extra_ways(['compacting_gc']),
|
| 704 | 704 | compile_and_run, [''])
|
| 705 | + |
|
| 706 | +test('T19048',
|
|
| 707 | + [ omit_ghci
|
|
| 708 | + , no_check_hp
|
|
| 709 | + , js_skip
|
|
| 710 | + , extra_run_opts('+RTS -hT -i0 -RTS')
|
|
| 711 | + ],
|
|
| 712 | + compile_and_run, ['-O -rtsopts']) |