LibGC: Allocate HeapBlocks from 2 MiB chunks instead of one mmap each
Previously every 16 KiB HeapBlock was its own posix_memalign / mach_vm_map / VirtualAlloc, which churned VMAs and made the kernel's vm_area_struct list balloon for any non-trivial heap. Carve slots out of 2 MiB chunks instead. The kernel now sees one mmap per 128 blocks. Chunks are owned exclusively by a single BlockAllocator and are never released back to the OS or shared across allocators -- that's how we keep the heap's VM permanently type-isolated, where a virtual address used for a cell of type T is never reused for any other type. We don't bother tracking chunk bases for teardown: the destructor leaks them by design. Per-block memory return is preserved: deallocate_block still calls MADV_FREE_REUSABLE / MADV_FREE / MADV_DONTNEED / DiscardVirtualMemory so the kernel can reclaim physical pages under pressure.
This commit is contained in:
parent
98a9c36ba7
commit
788bfe0f24
1 changed files with 65 additions and 50 deletions
|
|
@ -30,66 +30,78 @@
|
|||
|
||||
namespace GC {
|
||||
|
||||
BlockAllocator::~BlockAllocator()
|
||||
{
|
||||
for (auto* block : m_blocks) {
|
||||
ASAN_UNPOISON_MEMORY_REGION(block, HeapBlock::BLOCK_SIZE);
|
||||
#if defined(AK_OS_MACOS)
|
||||
kern_return_t kr = mach_vm_deallocate(mach_task_self(), reinterpret_cast<mach_vm_address_t>(block), HeapBlock::BLOCK_SIZE);
|
||||
VERIFY(kr == KERN_SUCCESS);
|
||||
#elif defined(AK_OS_WINDOWS)
|
||||
if (!VirtualFree(block, 0, MEM_RELEASE)) {
|
||||
warnln("{}", Error::from_windows_error());
|
||||
VERIFY_NOT_REACHED();
|
||||
}
|
||||
#else
|
||||
free(block);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
// Each BlockAllocator carves its 16 KiB HeapBlock slots out of 2 MiB
|
||||
// chunks, so the kernel sees one mmap per 128 blocks instead of one per
|
||||
// block. Chunks are owned exclusively by a single BlockAllocator and are
|
||||
// never released back to the OS or shared across allocators -- that's how
|
||||
// we keep the heap's VM permanently type-isolated, where a virtual address
|
||||
// used for a cell of type T is never reused for any other type.
|
||||
//
|
||||
// We do not hint MADV_HUGEPAGE: per-block madvise() in deallocate_block
|
||||
// would split any THP backing the chunk anyway. Per-block memory return
|
||||
// matches what V8, SpiderMonkey, and WebKit's libpas all do.
|
||||
static constexpr size_t CHUNK_SIZE = 2 * MiB;
|
||||
static constexpr size_t BLOCKS_PER_CHUNK = CHUNK_SIZE / HeapBlock::BLOCK_SIZE;
|
||||
static_assert(CHUNK_SIZE % HeapBlock::BLOCK_SIZE == 0);
|
||||
static_assert(BLOCKS_PER_CHUNK == 128);
|
||||
|
||||
BlockAllocator::~BlockAllocator() = default;
|
||||
|
||||
void* BlockAllocator::allocate_block([[maybe_unused]] char const* name)
|
||||
{
|
||||
if (!m_blocks.is_empty()) {
|
||||
// To reduce predictability, take a random block from the cache.
|
||||
size_t random_index = get_random_uniform(m_blocks.size());
|
||||
auto* block = m_blocks.unstable_take(random_index);
|
||||
ASAN_UNPOISON_MEMORY_REGION(block, HeapBlock::BLOCK_SIZE);
|
||||
LSAN_REGISTER_ROOT_REGION(block, HeapBlock::BLOCK_SIZE);
|
||||
if (m_blocks.is_empty()) {
|
||||
void* chunk_base = nullptr;
|
||||
#if defined(AK_OS_MACOS)
|
||||
mach_vm_address_t address = 0;
|
||||
kern_return_t kr = mach_vm_map(
|
||||
mach_task_self(),
|
||||
&address,
|
||||
CHUNK_SIZE,
|
||||
CHUNK_SIZE - 1,
|
||||
VM_FLAGS_ANYWHERE,
|
||||
MEMORY_OBJECT_NULL,
|
||||
0,
|
||||
false,
|
||||
VM_PROT_READ | VM_PROT_WRITE,
|
||||
VM_PROT_READ | VM_PROT_WRITE,
|
||||
VM_INHERIT_DEFAULT);
|
||||
VERIFY(kr == KERN_SUCCESS);
|
||||
chunk_base = reinterpret_cast<void*>(address);
|
||||
#elif defined(AK_OS_WINDOWS)
|
||||
chunk_base = VirtualAlloc(nullptr, CHUNK_SIZE, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
|
||||
VERIFY(chunk_base);
|
||||
#else
|
||||
auto rc = posix_memalign(&chunk_base, CHUNK_SIZE, CHUNK_SIZE);
|
||||
VERIFY(rc == 0);
|
||||
#endif
|
||||
|
||||
#if defined(MADV_FREE_REUSE) && defined(MADV_FREE_REUSABLE)
|
||||
if (madvise(block, HeapBlock::BLOCK_SIZE, MADV_FREE_REUSE) < 0) {
|
||||
perror("madvise(MADV_FREE_REUSE)");
|
||||
// Mark the whole chunk reusable upfront so MADV_FREE_REUSE pairs
|
||||
// symmetrically when slots are popped from m_blocks below. (Linux
|
||||
// and Windows fall through with no-op.)
|
||||
if (madvise(chunk_base, CHUNK_SIZE, MADV_FREE_REUSABLE) < 0) {
|
||||
perror("madvise(MADV_FREE_REUSABLE)");
|
||||
VERIFY_NOT_REACHED();
|
||||
}
|
||||
#endif
|
||||
return block;
|
||||
|
||||
ASAN_POISON_MEMORY_REGION(chunk_base, CHUNK_SIZE);
|
||||
for (size_t i = 0; i < BLOCKS_PER_CHUNK; ++i)
|
||||
m_blocks.append(static_cast<u8*>(chunk_base) + i * HeapBlock::BLOCK_SIZE);
|
||||
}
|
||||
|
||||
#if defined(AK_OS_MACOS)
|
||||
mach_vm_address_t address = 0;
|
||||
kern_return_t kr = mach_vm_map(
|
||||
mach_task_self(),
|
||||
&address,
|
||||
HeapBlock::BLOCK_SIZE,
|
||||
HeapBlock::BLOCK_SIZE - 1,
|
||||
VM_FLAGS_ANYWHERE,
|
||||
MEMORY_OBJECT_NULL,
|
||||
0,
|
||||
false,
|
||||
VM_PROT_READ | VM_PROT_WRITE,
|
||||
VM_PROT_READ | VM_PROT_WRITE,
|
||||
VM_INHERIT_DEFAULT);
|
||||
VERIFY(kr == KERN_SUCCESS);
|
||||
auto* block = reinterpret_cast<void*>(address);
|
||||
#elif defined(AK_OS_WINDOWS)
|
||||
auto* block = VirtualAlloc(NULL, HeapBlock::BLOCK_SIZE, MEM_COMMIT, PAGE_READWRITE);
|
||||
VERIFY(block);
|
||||
#else
|
||||
void* block = nullptr;
|
||||
auto rc = posix_memalign(&block, HeapBlock::BLOCK_SIZE, HeapBlock::BLOCK_SIZE);
|
||||
VERIFY(rc == 0);
|
||||
#endif
|
||||
// Random pick to preserve the previous anti-predictability behavior.
|
||||
size_t random_index = get_random_uniform(m_blocks.size());
|
||||
auto* block = m_blocks.unstable_take(random_index);
|
||||
|
||||
ASAN_UNPOISON_MEMORY_REGION(block, HeapBlock::BLOCK_SIZE);
|
||||
LSAN_REGISTER_ROOT_REGION(block, HeapBlock::BLOCK_SIZE);
|
||||
#if defined(MADV_FREE_REUSE) && defined(MADV_FREE_REUSABLE)
|
||||
if (madvise(block, HeapBlock::BLOCK_SIZE, MADV_FREE_REUSE) < 0) {
|
||||
perror("madvise(MADV_FREE_REUSE)");
|
||||
VERIFY_NOT_REACHED();
|
||||
}
|
||||
#endif
|
||||
return block;
|
||||
}
|
||||
|
||||
|
|
@ -97,6 +109,9 @@ void BlockAllocator::deallocate_block(void* block)
|
|||
{
|
||||
VERIFY(block);
|
||||
|
||||
// Tell the kernel it can reclaim physical pages backing this 16 KiB
|
||||
// slot. The slot stays in m_blocks for reuse by this same
|
||||
// BlockAllocator -- never seen by a different cell type.
|
||||
#if defined(AK_OS_WINDOWS)
|
||||
DWORD ret = DiscardVirtualMemory(block, HeapBlock::BLOCK_SIZE);
|
||||
if (ret != ERROR_SUCCESS) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue