Skip to content

Commit 787be2a

Browse files
robobunJarred-Sumner
authored andcommitted
arena: allocate the per-bin abandoned-page bitmaps on first abandon
Every (heap, arena) pair carried MI_ARENA_BIN_COUNT abandoned-page bitmaps next to its `pages` bitmap, laid out and initialized up front: once in the arena's info slices for the main heap, and once more in a zeroed allocation for every other heap that touched the arena (`mi_heap_ensure_arena_pages`). Each bitmap is sized by the arena (2 KiB per GiB of arena), so with the default 1 GiB reservation that is ~110 KiB per heap, and ~410 KiB for a heap in a 4 GiB arena. Writing the header of each bitmap touched one OS page per bitmap, so a heap paid about 50 page faults (or the memset, for external memory that is not known to be zero) before its first allocation, and most heaps never abandon a page at all: short-lived heaps are destroyed, not abandoned. Bun creates one heap per transpile and one for JSC's 4 GiB structure heap, so this was about 250 of the ~1000 page faults of `bun file.js`. Now `pages_abandoned[bin]` starts NULL and is allocated the first time a page of that bin is abandoned, from the subproc meta-data heap (safe on the abandon paths, where a regular heap allocation is not), published with a CAS. Readers treat NULL as all-clear; they were already guarded by `heap->abandoned_count[bin]`. `_mi_arena_pages_free` frees them with the heap's arena pages. If the allocation fails the page is abandoned unmapped, like a full page, and mapped once a free brings it back. The new test-abandoned-lazy exercises the abandon, reclaim, re-abandon, visit and delete paths from several threads at once.
1 parent 2c98a55 commit 787be2a

6 files changed

Lines changed: 287 additions & 25 deletions

File tree

CMakeLists.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -877,7 +877,7 @@ if (MI_BUILD_TESTS)
877877
enable_testing()
878878

879879
# static link tests
880-
set(mi_static_tests api api-fill stress-heaps stress-subprocs stress heap-mt heap-teardown heap-delete-race heap-churn heap-aba heap-burst-destroy fork-user-heap snapshot prof prof-adversarial purge-zero park-handoff free-before-init)
880+
set(mi_static_tests api api-fill stress-heaps stress-subprocs stress heap-mt heap-teardown heap-delete-race heap-churn heap-aba heap-burst-destroy abandoned-lazy fork-user-heap snapshot prof prof-adversarial purge-zero park-handoff free-before-init)
881881
if(NOT (MI_DEBUG_TSAN OR MI_TRACK_ASAN OR MI_DEBUG_UBSAN))
882882
list(APPEND mi_static_tests thp-optout) # counts madvise calls by interposing it, which a sanitizer runtime does first
883883
endif()

include/mimalloc/internal.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,7 @@ void _mi_arenas_abandoned_page_free(mi_page_t* page, mi_theap_t* curren
277277
void _mi_arenas_page_abandon(mi_page_t* page, mi_theap_t* current_theap);
278278
void _mi_arenas_page_unabandon(mi_page_t* page, mi_theap_t* current_theapx /* can be NULL */);
279279
bool _mi_arenas_page_try_reabandon_to_mapped(mi_page_t* page);
280+
void _mi_arena_pages_free(mi_arena_pages_t* arena_pages);
280281
size_t mi_arenas_get_count(mi_subproc_t* subproc);
281282
uint8_t* mi_arena_slice_start(mi_arena_t* arena, size_t slice_index);
282283

include/mimalloc/types.h

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -756,8 +756,13 @@ typedef struct mi_bbitmap_s mi_bbitmap_t; // atomic binned bitmap (defined in
756756

757757
struct mi_arena_pages_s {
758758
mi_bitmap_t* pages; // all registered pages (abandoned and owned)
759-
mi_bitmap_t* pages_abandoned[MI_ARENA_BIN_COUNT]; // abandoned pages per size bin (a set bit means the start of the page)
760-
// followed by the bitmaps (whose siz`es depend on the arena size)
759+
// Abandoned pages per size bin (a set bit means the start of the page). Each bitmap is
760+
// allocated the first time a page of that bin is abandoned (`mi_arena_pages_abandoned_ensure`);
761+
// NULL means no page of that bin was ever abandoned. Eagerly laying out all MI_ARENA_BIN_COUNT
762+
// of them cost a page fault per bitmap (one header write each, on its own OS page) for every
763+
// heap that touched an arena, and most heaps never abandon a page.
764+
_Atomic(mi_bitmap_t*) pages_abandoned[MI_ARENA_BIN_COUNT];
765+
// followed by the `pages` bitmap (whose size depends on the arena size)
761766
};
762767

763768

src/arena.c

Lines changed: 77 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ The arena allocation needs to be thread safe and we use an atomic bitmap to allo
2929
#error "The page_t.page_ma_offset field is not large enough to cover a full arena"
3030
#endif
3131

32+
static mi_bitmap_t* mi_arena_pages_abandoned(mi_arena_pages_t* arena_pages, size_t bin);
33+
static mi_bitmap_t* mi_arena_pages_abandoned_ensure(mi_arena_t* arena, mi_arena_pages_t* arena_pages, size_t bin);
34+
3235
/* -----------------------------------------------------------
3336
Arena id's
3437
----------------------------------------------------------- */
@@ -726,9 +729,9 @@ static mi_page_t* mi_arenas_page_try_find_abandoned(mi_theap_t* theap, size_t sl
726729
mi_forall_suitable_arenas(heap, req_arena, tseq, match_numa, any_numa, allow_large, arena)
727730
{
728731
mi_arena_pages_t* const arena_pages = mi_heap_arena_pages(heap, arena);
729-
if (arena_pages != NULL) {
732+
mi_bitmap_t* const bitmap = (arena_pages != NULL ? mi_arena_pages_abandoned(arena_pages, bin) : NULL);
733+
if (bitmap != NULL) {
730734
size_t slice_index;
731-
mi_bitmap_t* const bitmap = arena_pages->pages_abandoned[bin];
732735

733736
if (mi_bitmap_try_find_and_claim(bitmap, tseq, &slice_index, &mi_arena_try_claim_abandoned, arena)) {
734737
// found an abandoned page of the right size
@@ -1119,7 +1122,7 @@ static void mi_arenas_page_free_prim(mi_page_t* page, mi_subproc_t* subproc, mi_
11191122
const size_t bin = _mi_bin(mi_page_block_size(page));
11201123
mi_assert_internal(mi_bbitmap_is_clearN(arena->slices_free, slice_index, slice_count));
11211124
mi_assert_internal(mi_page_slice_committed(page) > 0 || mi_bitmap_is_setN(arena->slices_committed, slice_index, slice_count));
1122-
mi_assert_internal(bin >= MI_ARENA_BIN_COUNT || mi_bitmap_is_clearN(arena_pages->pages_abandoned[bin], slice_index, 1));
1125+
mi_assert_internal(bin >= MI_ARENA_BIN_COUNT || mi_arena_pages_abandoned(arena_pages, bin) == NULL || mi_bitmap_is_clearN(mi_arena_pages_abandoned(arena_pages, bin), slice_index, 1));
11231126
// note: we cannot check for `!mi_page_is_abandoned_and_mapped` since that may
11241127
// be (temporarily) not true if the free happens while trying to reclaim
11251128
// see `mi_arena_try_claim_abandoned`
@@ -1224,13 +1227,18 @@ void _mi_arenas_page_abandon(mi_page_t* page, mi_theap_t* current_theapx) {
12241227
mi_assert_internal(mi_page_slice_committed(page) > 0 || mi_bitmap_is_setN(arena->slices_committed, slice_index, slice_count));
12251228
mi_assert_internal(mi_bitmap_is_setN(arena->slices_dirty, slice_index, slice_count));
12261229

1227-
mi_page_set_abandoned_mapped(page);
1228-
const bool was_clear = mi_bitmap_set(arena_pages->pages_abandoned[bin], slice_index);
1229-
MI_UNUSED(was_clear); mi_assert_internal(was_clear);
1230-
mi_atomic_increment_relaxed(&heap->abandoned_count[bin]);
1231-
mi_theapx_stat_increase(heap, current_theapx, pages_abandoned, 1);
1232-
mi_abandoned_page_unown(page, current_theapx);
1233-
return;
1230+
// If the bin's bitmap cannot be allocated the page is abandoned unmapped, like a full
1231+
// page: it is still reachable through `pages` and is reclaimed once a block in it is freed.
1232+
mi_bitmap_t* const bitmap = mi_arena_pages_abandoned_ensure(arena, arena_pages, bin);
1233+
if mi_likely(bitmap != NULL) {
1234+
mi_page_set_abandoned_mapped(page);
1235+
const bool was_clear = mi_bitmap_set(bitmap, slice_index);
1236+
MI_UNUSED(was_clear); mi_assert_internal(was_clear);
1237+
mi_atomic_increment_relaxed(&heap->abandoned_count[bin]);
1238+
mi_theapx_stat_increase(heap, current_theapx, pages_abandoned, 1);
1239+
mi_abandoned_page_unown(page, current_theapx);
1240+
return;
1241+
}
12341242
}
12351243
}
12361244
// otherwise,
@@ -1298,7 +1306,9 @@ void _mi_arenas_page_unabandon(mi_page_t* page, mi_theap_t* current_theapx) {
12981306
mi_assert_internal(mi_page_slice_committed(page) > 0 || mi_bitmap_is_setN(arena->slices_committed, slice_index, slice_count));
12991307

13001308
// this busy waits until a concurrent reader (from alloc_abandoned) is done
1301-
mi_bitmap_clear_once_set(arena->subproc, arena_pages->pages_abandoned[bin], slice_index);
1309+
mi_bitmap_t* const bitmap = mi_arena_pages_abandoned(arena_pages, bin);
1310+
mi_assert_internal(bitmap != NULL); // a mapped page was set in it
1311+
mi_bitmap_clear_once_set(arena->subproc, bitmap, slice_index);
13021312
mi_page_clear_abandoned_mapped(page);
13031313
mi_atomic_decrement_relaxed(&heap->abandoned_count[bin]);
13041314
}
@@ -1384,7 +1394,8 @@ void _mi_arenas_purge_abandoned_holes(mi_heap_t* heap, mi_tld_t* tld) {
13841394
// singleton bins have no abandoned bitmap (upstream ad1bcdbf, to shrink arena meta).
13851395
for (size_t bin = 0; bin < MI_ARENA_BIN_COUNT; bin++) {
13861396
if (mi_atomic_load_relaxed(&heap->abandoned_count[bin]) == 0) continue;
1387-
mi_bitmap_t* const bitmap = arena_pages->pages_abandoned[bin];
1397+
mi_bitmap_t* const bitmap = mi_arena_pages_abandoned(arena_pages, bin);
1398+
if (bitmap == NULL) continue;
13881399
mi_purge_holes_arg_t parg = { bitmap, tld };
13891400
(void)_mi_bitmap_forall_set(bitmap, &mi_arena_page_purge_holes_at, arena, &parg);
13901401
}
@@ -1427,7 +1438,8 @@ void _mi_arenas_holes_report(mi_heap_t* heap, mi_holes_report_t* rep) {
14271438
if (arena_pages != NULL) {
14281439
for (size_t bin = 0; bin < MI_ARENA_BIN_COUNT; bin++) { // see above: not MI_BIN_COUNT
14291440
if (mi_atomic_load_relaxed(&heap->abandoned_count[bin]) == 0) continue;
1430-
mi_arena_holes_report_arg_t ra = { arena_pages->pages_abandoned[bin], rep };
1441+
mi_arena_holes_report_arg_t ra = { mi_arena_pages_abandoned(arena_pages, bin), rep };
1442+
if (ra.bitmap == NULL) continue;
14311443
(void)_mi_bitmap_forall_set(ra.bitmap, &mi_arena_page_holes_report_at, arena, &ra);
14321444
}
14331445
}
@@ -1651,8 +1663,7 @@ static size_t mi_arena_pages_size(size_t slice_count, size_t* bitmap_base) {
16511663
if (slice_count == 0) slice_count = MI_BCHUNK_BITS;
16521664
mi_assert_internal((slice_count % MI_BCHUNK_BITS) == 0);
16531665
const size_t base_size = _mi_align_up(sizeof(mi_arena_pages_t), MI_BCHUNK_SIZE);
1654-
const size_t bitmaps_count = 1 + MI_ARENA_BIN_COUNT; // pages, and abandoned
1655-
const size_t bitmaps_size = bitmaps_count * mi_bitmap_size(slice_count, NULL);
1666+
const size_t bitmaps_size = mi_bitmap_size(slice_count, NULL); // pages (the abandoned bitmaps are allocated on demand)
16561667
const size_t size = base_size + bitmaps_size;
16571668
if (bitmap_base != NULL) *bitmap_base = base_size;
16581669
return size;
@@ -1662,7 +1673,7 @@ static size_t mi_arena_info_slices_needed(size_t slice_count, size_t* bitmap_bas
16621673
if (slice_count == 0) slice_count = MI_BCHUNK_BITS;
16631674
mi_assert_internal((slice_count % MI_BCHUNK_BITS) == 0);
16641675
const size_t base_size = _mi_align_up(sizeof(mi_arena_t), MI_BCHUNK_SIZE);
1665-
const size_t bitmaps_count = 4 + MI_ARENA_BIN_COUNT; // commit, dirty, purge, pages, and abandoned
1676+
const size_t bitmaps_count = 4; // commit, dirty, purge, and pages (the abandoned bitmaps are allocated on demand)
16661677
const size_t bitmaps_size = bitmaps_count * mi_bitmap_size(slice_count, NULL) + mi_bbitmap_size(slice_count, NULL); // + free
16671678
#if MI_PAGE_META_IS_SEPARATED
16681679
const size_t pages_size = slice_count * sizeof(mi_page_t);
@@ -1701,12 +1712,55 @@ static mi_arena_pages_t* mi_arena_pages_alloc(mi_arena_t* arena) {
17011712
uint8_t* base = (uint8_t*)arena_pages + bitmap_base;
17021713
mi_assert_internal(_mi_is_aligned(base, MI_BCHUNK_SIZE));
17031714
arena_pages->pages = mi_arena_bitmap_init(slice_count, &base);
1704-
for (size_t i = 0; i < MI_ARENA_BIN_COUNT; i++) {
1705-
arena_pages->pages_abandoned[i] = mi_arena_bitmap_init(slice_count, &base);
1706-
}
1715+
// `pages_abandoned[]` stays NULL (the allocation is zeroed) until a page of that bin is abandoned.
17071716
return arena_pages;
17081717
}
17091718

1719+
// The abandoned-pages bitmap of `bin`, or NULL if no page of that bin was ever abandoned in
1720+
// this (heap, arena) pair. A NULL bitmap reads as all-clear.
1721+
static mi_bitmap_t* mi_arena_pages_abandoned(mi_arena_pages_t* arena_pages, size_t bin) {
1722+
mi_assert_internal(bin < MI_ARENA_BIN_COUNT);
1723+
return mi_atomic_load_ptr_acquire(mi_bitmap_t, &arena_pages->pages_abandoned[bin]);
1724+
}
1725+
1726+
// The abandoned-pages bitmap of `bin`, allocated on first use. Allocated from the subproc
1727+
// meta-data heap so this is safe on the abandon paths (a thread tearing down its theaps, a
1728+
// foreign free re-abandoning a page), where allocating from a regular heap is not. Publishing
1729+
// is a CAS so no lock is needed: a loser frees its copy and uses the winner's. Returns NULL only
1730+
// if the allocation failed.
1731+
static mi_bitmap_t* mi_arena_pages_abandoned_ensure(mi_arena_t* arena, mi_arena_pages_t* arena_pages, size_t bin) {
1732+
mi_bitmap_t* bitmap = mi_arena_pages_abandoned(arena_pages, bin);
1733+
if mi_likely(bitmap != NULL) return bitmap;
1734+
const size_t slice_count = arena->slice_count;
1735+
const size_t size = mi_bitmap_size(slice_count, NULL);
1736+
mi_bitmap_t* fresh = (mi_bitmap_t*)_mi_meta_zalloc_aligned(arena->subproc, size, MI_BCHUNK_SIZE, NULL);
1737+
if (fresh == NULL) return NULL;
1738+
mi_bitmap_init(fresh, slice_count, true /* already zero */);
1739+
mi_bitmap_t* expected = NULL;
1740+
if (mi_atomic_cas_ptr_strong_acq_rel(mi_bitmap_t, &arena_pages->pages_abandoned[bin], &expected, fresh)) {
1741+
return fresh;
1742+
}
1743+
// another thread published one first
1744+
_mi_free_subproc_safe(fresh);
1745+
mi_assert_internal(expected != NULL);
1746+
return expected;
1747+
}
1748+
1749+
// Release the on-demand abandoned bitmaps of a heap's arena pages (the `pages` bitmap is part of
1750+
// the `arena_pages` allocation itself).
1751+
static void mi_arena_pages_free_abandoned(mi_arena_pages_t* arena_pages) {
1752+
for (size_t bin = 0; bin < MI_ARENA_BIN_COUNT; bin++) {
1753+
mi_bitmap_t* bitmap = mi_atomic_exchange_ptr_acq_rel(mi_bitmap_t, &arena_pages->pages_abandoned[bin], NULL);
1754+
if (bitmap != NULL) { _mi_free_subproc_safe(bitmap); }
1755+
}
1756+
}
1757+
1758+
void _mi_arena_pages_free(mi_arena_pages_t* arena_pages) {
1759+
if (arena_pages == NULL) return;
1760+
mi_arena_pages_free_abandoned(arena_pages);
1761+
_mi_free_subproc_safe(arena_pages);
1762+
}
1763+
17101764
static mi_arena_t* mi_arena_initialize(mi_subproc_t* subproc, void* start,
17111765
size_t slice_count, mi_arena_t* parent, size_t total_size,
17121766
int numa_node, bool exclusive,
@@ -1799,7 +1853,7 @@ static mi_arena_t* mi_arena_initialize(mi_subproc_t* subproc, void* start,
17991853
arena->slices_purge = mi_arena_bitmap_init(slice_count, &base);
18001854
arena->pages_main.pages = mi_arena_bitmap_init(slice_count, &base);
18011855
for (size_t i = 0; i < MI_ARENA_BIN_COUNT; i++) {
1802-
arena->pages_main.pages_abandoned[i] = mi_arena_bitmap_init(slice_count, &base);
1856+
mi_atomic_store_ptr_relaxed(mi_bitmap_t, &arena->pages_main.pages_abandoned[i], NULL); // allocated on first abandon
18031857
}
18041858
#if MI_PAGE_META_IS_SEPARATED
18051859
arena->pages_meta = (mi_page_t*)base;
@@ -2690,7 +2744,9 @@ bool _mi_heap_visit_blocks(mi_heap_t* heap, bool abandoned_only, bool visit_bloc
26902744
for (size_t bin = 0; ok && bin < MI_ARENA_BIN_COUNT; bin++) {
26912745
// todo: if we had a single abandoned page map as well, this can be faster.
26922746
if (mi_atomic_load_relaxed(&heap->abandoned_count[bin]) > 0) {
2693-
ok = _mi_bitmap_forall_set(arena_pages->pages_abandoned[bin], &mi_heap_visit_page_at, arena, &visit_info);
2747+
mi_bitmap_t* const bitmap = mi_arena_pages_abandoned(arena_pages, bin);
2748+
if (bitmap == NULL) continue;
2749+
ok = _mi_bitmap_forall_set(bitmap, &mi_heap_visit_page_at, arena, &visit_info);
26942750
}
26952751
}
26962752
}

src/heap.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,7 @@ static void mi_heap_free(mi_heap_t* heap, bool acquire_heaps_lock) {
229229
mi_arena_pages_t* arena_pages = mi_atomic_load_ptr_relaxed(mi_arena_pages_t, &heap->arena_pages[i]);
230230
if (arena_pages!=NULL) {
231231
mi_atomic_store_ptr_relaxed(mi_arena_pages_t, &heap->arena_pages[i], NULL);
232-
_mi_free_subproc_safe(arena_pages);
232+
_mi_arena_pages_free(arena_pages);
233233
}
234234
}
235235
}

0 commit comments

Comments
 (0)