Marge Bot pushed to branch master at Glasgow Haskell Compiler / GHC
Commits:
-
4bd193fa
by Sylvain Henry at 2026-08-13T10:17:00-04:00
11 changed files:
- changelog.d/fix-heap-census-large-arrays-19048
- rts/LdvProfile.c
- rts/PrimOps.cmm
- rts/Printer.c
- rts/ProfHeap.c
- rts/include/rts/storage/ClosureMacros.h
- rts/sm/Sanity.c
- rts/sm/Storage.c
- + testsuite/tests/rts/T27585.hs
- + testsuite/tests/rts/T27585.stdout
- testsuite/tests/rts/all.T
Changes:
| ... | ... | @@ -2,5 +2,5 @@ section: rts |
| 2 | 2 | synopsis: Correctly mark slop bytes when shrinking large arrays.
|
| 3 | 3 | Heap census no longer traverses garbage-collected closures when profiling
|
| 4 | 4 | is off.
|
| 5 | -issues: #19048
|
|
| 6 | -mrs: !15685 |
|
| 5 | +issues: #19048 #27585
|
|
| 6 | +mrs: !15685 !16452 |
| ... | ... | @@ -177,8 +177,8 @@ processHeapForDead( bdescr *bd ) |
| 177 | 177 | p = bd->start;
|
| 178 | 178 | while (p < bd->free) {
|
| 179 | 179 | p += processHeapClosureForDead((StgClosure *)p);
|
| 180 | - while (p < bd->free && !*p) // skip slop
|
|
| 181 | - p++;
|
|
| 180 | + // See Note [Skipping slop when scanning the heap] in ClosureMacros.h
|
|
| 181 | + p = skipSlop(p, bd->free);
|
|
| 182 | 182 | }
|
| 183 | 183 | ASSERT(p == bd->free);
|
| 184 | 184 | bd = bd->link;
|
| ... | ... | @@ -223,9 +223,8 @@ stg_isMutableByteArrayWeaklyPinnedzh ( gcptr mba ) |
| 223 | 223 | /* Note [shrink-array slop marker]
|
| 224 | 224 | * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
| 225 | 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.
|
|
| 226 | + * slop at address `slop_start`, we write an O(1) marker so that linear heap
|
|
| 227 | + * scans can skip over the slop without reading stale heap pointers.
|
|
| 229 | 228 | *
|
| 230 | 229 | * The marker scheme (let n = number of slop words):
|
| 231 | 230 | *
|
| ... | ... | @@ -239,8 +238,8 @@ stg_isMutableByteArrayWeaklyPinnedzh ( gcptr mba ) |
| 239 | 238 | *
|
| 240 | 239 | * An array may be shrunk multiple times, leaving consecutive slop regions.
|
| 241 | 240 | * 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).
|
|
| 241 | + * to the next live closure. See Note [Skipping slop when scanning the heap]
|
|
| 242 | + * in ClosureMacros.h.
|
|
| 244 | 243 | */
|
| 245 | 244 | |
| 246 | 245 | // shrink size of MutableByteArray in-place
|
| ... | ... | @@ -1001,19 +1001,9 @@ 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 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 | - }
|
|
| 1004 | + // See Note [Skipping slop when scanning the heap]
|
|
| 1005 | + // in ClosureMacros.h
|
|
| 1006 | + r = skipSlop(r, bd->free);
|
|
| 1017 | 1007 | if (!LOOKS_LIKE_CLOSURE_PTR(r)) {
|
| 1018 | 1008 | debugBelch("%p found at %p, no closure at %p\n",
|
| 1019 | 1009 | p, q, r);
|
| ... | ... | @@ -1317,37 +1317,8 @@ heapCensusBlock(Census *census, bdescr *bd) |
| 1317 | 1317 | |
| 1318 | 1318 | p += size;
|
| 1319 | 1319 | |
| 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 | - }
|
|
| 1320 | + /* See Note [Skipping slop when scanning the heap] in ClosureMacros.h */
|
|
| 1321 | + p = skipSlop(p, bd->free);
|
|
| 1351 | 1322 | }
|
| 1352 | 1323 | }
|
| 1353 | 1324 | |
| ... | ... | @@ -1472,8 +1443,6 @@ heapCensusChain( Census *census, bdescr *bd ) |
| 1472 | 1443 | // of the associated block descriptor, thus introducing slop at the end
|
| 1473 | 1444 | // of the object. This slop remains after GC, violating the assumption
|
| 1474 | 1445 | // of the loop below that all slop has been eliminated (#11627).
|
| 1475 | - // The slop isn't always zeroed (e.g. in non-profiling mode, cf
|
|
| 1476 | - // OVERWRITING_CLOSURE_OFS).
|
|
| 1477 | 1446 | // Consequently, we handle large ARR_WORDS objects as a special case.
|
| 1478 | 1447 | if (bd->flags & BF_LARGE) {
|
| 1479 | 1448 | StgPtr p = bd->start;
|
| ... | ... | @@ -634,6 +634,44 @@ INLINE_HEADER void writeSlopMarker(StgWord *slop, StgWord n) |
| 634 | 634 | }
|
| 635 | 635 | }
|
| 636 | 636 | |
| 637 | +// Note [Skipping slop when scanning the heap]
|
|
| 638 | +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
| 639 | +// Every linear scan of the heap (heap census, LDV census, sanity checker,
|
|
| 640 | +// findPtr) has to step over slop between closures. Slop comes in two forms:
|
|
| 641 | +//
|
|
| 642 | +// 1. Zero words: alignment padding for large/pinned objects, zeroed
|
|
| 643 | +// explicitly (see MEMSET_SLOP_W in allocatePinned).
|
|
| 644 | +//
|
|
| 645 | +// 2. Slop markers: written by writeSlopMarker whenever a closure is
|
|
| 646 | +// overwritten by a smaller one. See Note [shrink-array slop marker]
|
|
| 647 | +// in PrimOps.cmm for the encoding.
|
|
| 648 | +//
|
|
| 649 | +// A closure can be shrunk repeatedly, leaving consecutive slop regions, so we
|
|
| 650 | +// must loop until reaching a word that can't be slop. Info pointers are never
|
|
| 651 | +// 0 or (StgWord)(-1), so such a word starts the next closure.
|
|
| 652 | +//
|
|
| 653 | +// All scanners must use skipSlop: if any one of them keeps its own copy of
|
|
| 654 | +// this loop it will go out of sync with the encoding (#27585).
|
|
| 655 | +INLINE_HEADER StgPtr skipSlop(StgPtr p, StgPtr end)
|
|
| 656 | +{
|
|
| 657 | + while (p < end) {
|
|
| 658 | + if (!*p) {
|
|
| 659 | + // single-word slop region, or alignment padding
|
|
| 660 | + p++;
|
|
| 661 | + } else if (*p == (StgWord)(-1)) {
|
|
| 662 | + // Multi-word slop region: sentinel, count, then that many words.
|
|
| 663 | + // writeSlopMarker only writes the sentinel for n >= 2, so the count
|
|
| 664 | + // word is always within the region. We assert rather than bail out
|
|
| 665 | + // so that a corrupt heap is reported instead of silently skipped.
|
|
| 666 | + ASSERT(p + 1 < end);
|
|
| 667 | + p += 2 + *(p + 1);
|
|
| 668 | + } else {
|
|
| 669 | + break;
|
|
| 670 | + }
|
|
| 671 | + }
|
|
| 672 | + return p;
|
|
| 673 | +}
|
|
| 674 | + |
|
| 637 | 675 | INLINE_HEADER void
|
| 638 | 676 | markImmutableSlop (StgClosure *p,
|
| 639 | 677 | uint32_t offset, /*< offset to start marking at, in words */
|
| ... | ... | @@ -605,19 +605,9 @@ void checkHeapChain (bdescr *bd) |
| 605 | 605 | ASSERT( size >= MIN_PAYLOAD_SIZE + sizeofW(StgHeader) );
|
| 606 | 606 | p += size;
|
| 607 | 607 | |
| 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 | - }
|
|
| 608 | + /* See Note [Skipping slop when scanning the heap]
|
|
| 609 | + in ClosureMacros.h */
|
|
| 610 | + p = skipSlop(p, bd->free);
|
|
| 621 | 611 | }
|
| 622 | 612 | }
|
| 623 | 613 | }
|
| ... | ... | @@ -1035,9 +1035,10 @@ accountAllocation(Capability *cap, W_ n) |
| 1035 | 1035 | * leave slop behind depending on the size of the closure being
|
| 1036 | 1036 | * overwritten. See Note [marking slop when overwriting immutable closures].
|
| 1037 | 1037 | *
|
| 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]
|
|
| 1038 | + * To allow the heap profiler, the LDV profiler and the sanity checker to
|
|
| 1039 | + * linearly scan over heap blocks, slop must be identifiable without reading
|
|
| 1040 | + * stale heap pointers.
|
|
| 1041 | + * See Note [Skipping slop when scanning the heap] in ClosureMacros.h
|
|
| 1041 | 1042 | *
|
| 1042 | 1043 | * Shrunk-array slop has a further, concurrent reader: the non-moving GC mark
|
| 1043 | 1044 | * thread scans SmallMutArrPtrs payloads while the mutator may be shrinking
|
| ... | ... | @@ -1207,7 +1208,7 @@ allocateMightFail (Capability *cap, W_ n) |
| 1207 | 1208 | * When profiling we zero the space used for alignment. This allows us to
|
| 1208 | 1209 | * traverse pinned blocks in the heap profiler.
|
| 1209 | 1210 | *
|
| 1210 | - * See Note [skipping slop in the heap profiler]
|
|
| 1211 | + * See Note [Skipping slop when scanning the heap] in ClosureMacros.h
|
|
| 1211 | 1212 | */
|
| 1212 | 1213 | #define MEMSET_SLOP_W(p, val, len_w) memset(p, val, (len_w) * sizeof(W_))
|
| 1213 | 1214 |
| 1 | +{-# LANGUAGE MagicHash, UnboxedTuples, BlockArguments #-}
|
|
| 2 | +module Main where
|
|
| 3 | + |
|
| 4 | +import GHC.Exts
|
|
| 5 | +import GHC.IO (IO(..))
|
|
| 6 | +import System.Mem (performMajorGC)
|
|
| 7 | + |
|
| 8 | +-- Lifted wrapper so SmallMutableArray# can be passed around.
|
|
| 9 | +data MArr = MArr (SmallMutableArray# RealWorld Integer)
|
|
| 10 | + |
|
| 11 | +-- Variant of T19048 for the LDV (biographical) profiler, +RTS -hb (#27585).
|
|
| 12 | +--
|
|
| 13 | +-- The array is promoted to the oldest generation *before* shrinking, so the
|
|
| 14 | +-- shrink-array slop marker is written into an old-generation block. The next
|
|
| 15 | +-- major GC then runs LdvCensusForDead, whose linear heap scan
|
|
| 16 | +-- (processHeapForDead in rts/LdvProfile.c) must skip the slop correctly.
|
|
| 17 | +--
|
|
| 18 | +-- The array must stay below LARGE_OBJECT_THRESHOLD (409 words): large objects
|
|
| 19 | +-- live on the large_objects chain, which the census does not scan linearly.
|
|
| 20 | +main :: IO ()
|
|
| 21 | +main = do
|
|
| 22 | + ma <- newArr
|
|
| 23 | + fillArr ma 299
|
|
| 24 | + -- Two major GCs promote the array to the oldest generation.
|
|
| 25 | + performMajorGC
|
|
| 26 | + performMajorGC
|
|
| 27 | + -- Shrink: writes the slop marker over slots [10..299], in place, in an
|
|
| 28 | + -- old-generation block.
|
|
| 29 | + shrinkArr ma
|
|
| 30 | + n <- getSize ma
|
|
| 31 | + putStrLn $ "size after shrink = " ++ show n
|
|
| 32 | + -- With -hb active, LdvCensusForDead scans the old blocks containing the
|
|
| 33 | + -- slop marker.
|
|
| 34 | + performMajorGC
|
|
| 35 | + x <- readElem ma 0
|
|
| 36 | + putStrLn $ "arr[0] = " ++ show x
|
|
| 37 | + putStrLn "survived"
|
|
| 38 | + |
|
| 39 | +newArr :: IO MArr
|
|
| 40 | +newArr = IO \s -> case newSmallArray# 300# (0 :: Integer) s of
|
|
| 41 | + (# s', ma #) -> (# s', MArr ma #)
|
|
| 42 | + |
|
| 43 | +-- Overwrite every slot with a distinct Integer so each holds a unique,
|
|
| 44 | +-- definitely non-zero heap pointer.
|
|
| 45 | +fillArr :: MArr -> Int -> IO ()
|
|
| 46 | +fillArr _ (-1) = pure ()
|
|
| 47 | +fillArr arr@(MArr ma) n@(I# n#) = do
|
|
| 48 | + IO \s -> case writeSmallArray# ma n# (fromIntegral n :: Integer) s of
|
|
| 49 | + s' -> (# s', () #)
|
|
| 50 | + fillArr arr (n - 1)
|
|
| 51 | + |
|
| 52 | +shrinkArr :: MArr -> IO ()
|
|
| 53 | +shrinkArr (MArr ma) = IO \s ->
|
|
| 54 | + case shrinkSmallMutableArray# ma 10# s of s' -> (# s', () #)
|
|
| 55 | + |
|
| 56 | +getSize :: MArr -> IO Int
|
|
| 57 | +getSize (MArr ma) = IO \s ->
|
|
| 58 | + case getSizeofSmallMutableArray# ma s of (# s', n# #) -> (# s', I# n# #)
|
|
| 59 | + |
|
| 60 | +readElem :: MArr -> Int -> IO Integer
|
|
| 61 | +readElem (MArr ma) (I# i#) = IO \s -> readSmallArray# ma i# s |
| 1 | +size after shrink = 10
|
|
| 2 | +arr[0] = 0
|
|
| 3 | +survived |
| ... | ... | @@ -710,3 +710,13 @@ test('T19048', |
| 710 | 710 | , extra_run_opts('+RTS -hT -i0 -RTS')
|
| 711 | 711 | ],
|
| 712 | 712 | compile_and_run, ['-O -rtsopts'])
|
| 713 | + |
|
| 714 | +test('T27585',
|
|
| 715 | + [ omit_ghci
|
|
| 716 | + , no_check_hp
|
|
| 717 | + , js_skip
|
|
| 718 | + , when(have_profiling(), extra_ways(['prof_hb']))
|
|
| 719 | + , only_ways(['prof_hb'])
|
|
| 720 | + , extra_run_opts('+RTS -i0 -RTS')
|
|
| 721 | + ],
|
|
| 722 | + compile_and_run, ['-O -rtsopts']) |