Repository navigation
mm: cache heap page blocks per CPU in front of the heap buddy - #114
Open
JiTianYu391 wants to merge 2 commits into
Open
JiTianYu391 wants to merge 2 commits into
JiTianYu391 wants to merge 2 commits into
Conversation
On a 5-vCPU KVM guest, malloc(64)+free scales from about 115 ns alone to
2,130 ns with five workers. Prior contention sampling found 70% of slab
cache lock acquisitions contended; pointer_owner also took heap.page_lock
on every free. Reproduce with kbench=3 and five alternating A/B boot pairs.
Give each of the 17 kmalloc size classes a per-CPU magazine capped at
32 objects / 16 KiB. Refill and spill in batches under the slab cache lock,
keep cached objects counted in slab->inuse so shrink cannot reclaim them,
and retain a CAS transition from ALLOCATED to CACHED to reject double free.
Typed/dynamic caches keep their existing constructor/destructor path.
Use stable buddy ownership tags for valid live pointers without taking the
page lock, and aggregate usage counters from the per-CPU front ends.
The new early malloc caller also exposed smp_init publishing cpu_count
before allocating cpus. GDB found smp_init -> aligned_alloc -> malloc ->
heap_cpu_cache -> get_current_cpu_id with cpu_count=5 and cpus=NULL, faulting
at cpus[0].lapic_id (address 8); panic then faults in send_ipi_all. Extend
the BSP fallback until the CPU table exists.
Validation: five alternating A/B boot pairs, three rounds per boot,
median within each boot then median across boots (ns/op):
A B
malloc/64/solo 115.4 99.4
malloc/64/all-cpus 2130.3 107.8
malloc/4096/solo 171.5 159.0
malloc/4096/all 2992.1 184.1
malloc/65536/solo 167.5 145.1
malloc/65536/all 2793.1 2382.0
yield/solo 92.0 92.5
Only the >20% changes (64 B and 4 KiB multi-CPU) establish a performance
gain; smaller differences are treated as noise. All ten boots complete.
Every boot passes heap_validate, all size classes with batch spills,
realloc data checks, dynamic slab busy/shrink/destroy, free poisoning and
exactly one expected duplicate-free rejection, with zero failures.
The comparison images use the same scheduler/kbench/pinning/strcpy test
overlays and SMP bootstrap guard; only the allocator differs. Temporary
integrity checks remain outside this PR. Standalone origin/develop build
uses CONFIG_VIRTIO_GPU off and -Wl,-u,strcpy for the existing LTO defect.
The rejected two-task steal minimum and idle polling are not included.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Every large allocation and every slab growth/shrink takes heap.page_lock, the single lock of the kernel heap's buddy allocator, and pointer_owner() took the same lock on every free just to read a page tag. With kbench on 5 vCPUs a 64 KiB malloc()/free() loop costs 144 ns on one CPU but 2364 ns per op when every CPU runs it (a lock-stat run showed ~45% of acquisitions of this lock finding it held). As in Linux's per-CPU pagesets, park freed blocks of orders 0..5 on the freeing CPU and hand them back from there, exchanging blocks with the buddy in batches. Cached blocks stay allocated in the buddy (they cannot coalesce) but have their tags cleared, so pointer_owner() and the double-free check reject them exactly like free blocks; page_free() claims the head tag with a compare-and-swap so a racing or repeated free of one block fails. When the buddy cannot satisfy an allocation all caches are drained once before giving up, and heap_get_stats() counts cached pages as free. pointer_owner() now reads the tag without page_lock (the tag of a valid pointer is stable while the block is allocated; invalid pointers are still rejected by the owner's magic and cookie checks). Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
JiTianYu391
force-pushed
the
perf/heap-page-cache
branch
from
October 23, 2026 11:17
634d636 to
cb18619
Compare
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
Every large
malloc(), every slab growth/shrink, and (viapointer_owner()) everyfree()takeheap.page_lock, the one lock of the kernel heap's buddy allocator. It does not scale: with the in-kernel benchmark from #110 (kbench, 5 vCPUs, KVM) a 64 KiBmalloc()+free()loop costs 144 ns/op on one CPU but 2364 ns/op when every CPU runs it, i.e. ~16x slower. A temporary lock-stat counter (not included) showed ~45% of acquisitions of this lock finding it already held; no other lock (scheduler.lock,frame_allocator.lock,printk_lock,pid_hash_lock) was contended in any kbench test.Fix (same idea as Linux's per-CPU pagesets)
page_lockis only taken on a miss or when the cache is full. Limits per order are 8/8/6/4/3/2 blocks.pointer_owner()and the double-free detection reject it exactly like a free block.page_free()claims the head tag with a compare-and-swap, so a repeated or racing free of one block fails instead of being cached twice.heap_get_stats()counts cached pages as free.pointer_owner()reads the page tag withoutpage_lock: the tag of a valid pointer is stable while its block is allocated, and an invalid pointer is still rejected by the owner's magic/cookie checks (the same change as in mm: scale kmalloc with bounded per-CPU magazines #112, which this PR is built on).Measurement
kbench, 5 vCPUs, A/B alternating, 5 boots per side, 3 rounds per boot, median of per-boot medians (>20% is the only effect I claim). Measured on
codex/perf-validation(develop + #108/#109/#111/#112 + #110) with this patch applied, because plain develop misbehaves under kbench without the scheduler fixes.The in-kernel heap self-check used during testing (17 size classes, batch overflow, realloc, exactly one expected rejected double free,
heap_validate()) reported 0 failures in all 10 boots.Why not solo-level yet
With the global statistics atomics (
allocation_calls,live_allocations,allocated_bytes,free_calls) temporarily disabled in the large-allocation path,malloc/65536/all-cpusmeasured 123 ns (= solo). The remaining 780 ns is those 6 shared-cacheline atomic operations per malloc/free pair, not a lock. Making them per-CPU belongs on top of #112 (which already has per-CPU counters for the slab path), so it is not part of this PR.Notes
mem/alloc.conly; no Kconfig, sizes the table with mm: scale kmalloc with bounded per-CPU magazines #112'sPCP_MAX_CPUS(CONFIG_FRAME_PCP_MAX_CPUS) (~64 KiB of bss at the default 256).strcpyLTO workaround (libs: retain strcpy for compiler-generated LTO references #109) to link; the stacked branch (mm: scale kmalloc with bounded per-CPU magazines #112 + this PR, on develop@153578b5) builds without new warnings and boots to the shell with 5 vCPUs under KVM.🤖 Generated with Claude Code