unix: only opt mappings out of THP when the system setting is always - #22
Closed
robobun wants to merge 1 commit into
Closed
unix: only opt mappings out of THP when the system setting is always#22robobun wants to merge 1 commit into
robobun wants to merge 1 commit into
Conversation
With allow_thp off, unix_mmap called madvise(MADV_NOHUGEPAGE) on every mapping it created. The kernel only backs a mapping that did not ask for huge pages with them when /sys/kernel/mm/transparent_hugepage/enabled is [always]. Under [madvise] (the Debian and Ubuntu default) and [never] the call changes nothing and costs one syscall per mmap: three during process initialization and one per arena after that. unix_detect_thp already reads that file. It now also records whether the setting is [always], and unix_mmap makes the madvise call only in that case. When the file cannot be read (no sysfs), the opt-out stays in place. The per-size mTHP settings are not read: that would take more syscalls than it saves. test-thp-optout interposes madvise, counts the MADV_NOHUGEPAGE calls the allocator makes during initialization (the ctest entry runs it with MIMALLOC_ALLOW_THP=0) and for a fresh reservation, and checks them against the setting of the machine it runs on. With allow_thp on it expects none.
Collaborator
This was referenced Aug 23, 2026
test-heap-aba: regression test for the theap cache ABA on a reused heap address (stacked on #25)
#26
Closed
Jarred-Sumner
added a commit
to oven-sh/bun
that referenced
this pull request
Aug 24, 2026
) (#40138) ### What does this PR do? Bumps mimalloc to oven-sh/mimalloc#27, which combines oven-sh/mimalloc#22–#26 and replaces their per-reader fixes with one teardown protocol for `mi_heap_delete` / `mi_heap_destroy`. The problem those PRs were circling: a heap is torn down while a concurrent cross-thread `mi_free`, or a thread that used the heap earlier and still caches a theap for it, can reach it. The hole in the middle was that the deleter claimed pages by *writing* to them (`atomic_or` of the owned bit) with nothing pinning the page, so a concurrent free could release the page and the slice be reused in between. Now: detach theaps → abandon their pages as thread-exit does → pin-then-claim every abandoned page (the same bitmap-as-pin protocol the abandoned-page map already uses) → free theaps → free heap. Details, contract and tests in the mimalloc PR. Also in the bump: mimalloc#22 (THP opt-out only when the system setting is `always` — saves a `madvise` per mmap on Debian/Ubuntu defaults), from mimalloc#23 only the scavenger signal mask (a fault on that thread produced no crash report), the scavenger thread starting lazily (a single-threaded `bun -e` no longer spawns it: −1 thread, −24 syscalls), and targeted upstream dev3 fixes (thread-locals-after-free guard, #1364, #1371, NUMA node count). Upstream's in-progress page-meta layout rework is deliberately *not* included. **What this does not do:** fix the Windows corrupted-free-list crash family (BUN-40BH and siblings). Those lists are written by Bun — #39897 (file read completing into a freed buffer, merged) and #39643 (poll handle freed twice from a nested event loop, open) — and mimalloc is only where the damage surfaces. #23's "validate links and cut the list" is not taken for that reason: it would keep running past the write and hide it. ### How did you verify your code works? - mimalloc `ctest`: Release 23/23, Debug (`MI_DEBUG_FULL`) 24/24 (was 20/21 on the old pin), ASAN 22/22, TSAN 19/19 with 0 reports (see mimalloc#27). - `bun bd test`: transpiler (190/190), bundler_edgecase (138/138), bundler_minify (43/43), css (2358 pass; 6 debug-timeout fuzz tests), workers/serve (same 4 failures as a `main` debug build on this box). - Release x64, n=7 interleaved, old pin vs new pin on the same Bun commit: | | old pin median (range) | new pin median (range) | Δ | |---|---|---|---| | `bun -e 1` peak RSS | 27024 KB (26564–27088) | 26016 KB (25984–26020)¹ | −3.7% | | `bun -e 1` syscalls | 270 | 246 | −24 (no `clone3` for the scavenger, −6 `rt_sigprocmask`, −5 `madvise`) | | `Bun.serve` hello RSS after 200k req (c=64) | 49760 KB (48540–50040) | 47948 KB (46384–48252) | −3.6% | | `bun build --minify --sourcemap` three.js×10 peak | 345696 KB (340672–348928) | 343724 KB (339612–348828) | −0.6% (overlaps) | ¹ one run at 17212 KB excluded from the range as an outlier. The first two rows come from the scavenger thread now starting on first use (first park / first scheduled purge) instead of at process init — a change made because the eager start aborted macOS processes that `DYLD_INSERT` the dylib (thread created before libobjc initializes); `bun -e 1` never needs it. Before that change the same A/B was flat (+0.1–0.3%, overlapping), so the teardown protocol itself is RSS-neutral as forecast.
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.
Problem
allow_thpoff (bun builds withMI_DEFAULT_ALLOW_THP=0),unix_mmapcallsmadvise(MADV_NOHUGEPAGE)on every mapping it creates (src/prim/unix/prim.c:492)./sys/kernel/mm/transparent_hugepage/enabledis[always]. Under[madvise](Debian and Ubuntu default) and[never]it changes nothing and costs one syscall per mmap: 3 at process initialization (bun --version), then 1 per arena.Fix
unix_detect_thpalready reads that file at startup. It now also records whether the setting is[always](unix_thp_needs_optout), andunix_mmapmakes the call only then. No syscall is added.always(hugepages-*kB/enabled) under another top-level setting is no longer opted out. Reading those files costs about 3 syscalls per size, more than this saves. The kernel default for every size isneverorinherit.test-thp-optout(new) fails onbun-dev3-v2on a[madvise]machine and passes with this change.[always]and a missing file still opt out (path redirected in a scratch copy). More runs in the notes.Background
unix_detect_thpalso feedshas_transparent_huge_pages. That flag only sets the purge granularity forallow_thp=2, so it keeps its meaning and the new flag is separate.madviseitself, which also catches the calls made from the static library. The ctest entry setsMIMALLOC_ALLOW_THP=0so the mappings made during initialization are counted too.Notes
_mi_prim_mem_initruns inmi_process_init_oncebefore the first mapping (page map, first arena), so the flag is set beforeunix_mmapreads it. Mappings made before that would still opt out because of the initial value.Measured on linux x64, kernel 6.17,
enabled=always [madvise] never, all mTHP sizesnever(2048kBinherit):test-thp-optoutwithMIMALLOC_ALLOW_THP=0, before:3 MADV_NOHUGEPAGE call(s)at initialization and1permi_reserve_os_memory. After:0and0. Withallow_thpon:0before and after.[always] madvise nevergives 3 and 1 (unchanged behavior),always [madvise] neverandalways madvise [never]give 0 and 0, file missing gives 3 and 1.prim.ccopied intovendor/mimallocand anLD_PRELOADshim that logsMADV_NOHUGEPAGE:bun --version3 calls to 0,bun -e 115 to 12. The release binary makes 7 forbun -e 1: 3 are mimalloc's, the others come from WTFOSAllocator::tryReserveUncommittedand from bun_alloc's bss arena, which are not touched here.src/static.ccompiles without warnings as C++ with bun's defines (clang), as C with gcc-Wpedantic, and withMADV_NOHUGEPAGEundefined.-DMI_EXTRA_CPPDEFS=_GNU_SOURCEfor theprof.cbuild issue noted in Keep the idle sweep's state on the tld instead of in __thread variables (fixes the Android SIGSEGV, oven-sh/bun#38051) #17): 18 of 21 pass. The 3 failures are the same with and without this change and are the ones listed in Keep the idle sweep's state on the tld instead of in __thread variables (fixes the Android SIGSEGV, oven-sh/bun#38051) #17:test-stress-subprocs(SIGSEGV),test-prof-adversarial(ld.sol_relocatedassertion),test-purge-holes(the 2 unformed-tail subtests).test-thp-optoutalso passes in aMI_TRACK_ASAN=ONbuild and in a DebugMI_DEBUG_FULL=ONbuild.madvise(MADV_NOHUGEPAGE)per mimalloc mmap. Once merged, bun picks it up by bumpingMIMALLOC_COMMITinscripts/build/deps/mimalloc.ts.