495 lines
20 KiB
Plaintext
495 lines
20 KiB
Plaintext
arena_delete :: (arena: *Arena) {
|
|
if (!is_valid(arena)) return;
|
|
#if ARENA_DEBUG {
|
|
free(arena.label,, context.default_allocator);
|
|
free(arena.file_path,, context.default_allocator);
|
|
}
|
|
|
|
bootstrapped := arena_is_bootstrapped(arena);
|
|
|
|
#if OS == .WINDOWS {
|
|
success := VirtualFree(arena.memory_base, 0, MEM_RELEASE) != .FALSE;
|
|
if !success {
|
|
value, description := System.get_error_value_and_string();
|
|
log_error("[Basic/Arena] In arena_delete, VirtualFree failed with code %: %\n", value, description);
|
|
return;
|
|
}
|
|
}
|
|
else #if OS_IS_UNIX {
|
|
success := P.munmap(arena.memory_base, cast(u64)reserve_size_bytes(arena)) == 0;
|
|
if !success {
|
|
value, description := System.get_error_value_and_string();
|
|
log_error("[Basic/Arena] In arena_delete, munmap failed with code %: %\n", value, description);
|
|
return;
|
|
}
|
|
}
|
|
|
|
if !bootstrapped arena.memory_base = null;
|
|
}
|
|
|
|
// Windows MEM_LARGE_PAGES and Darwin superpages both commit and wire the whole reservation at
|
|
// map time, and both refuse a partial decommit of it. Linux hugetlb pages can still be dropped
|
|
// with madvise, so only these two make free_pages_down_to a no-op.
|
|
LARGE_PAGES_ARE_FULLY_COMMITTED :: OS == .WINDOWS || OS == .MACOS;
|
|
|
|
#scope_module
|
|
UNQUERIED :: -1; // Sentinel for the cached large page size, which is legitimately 0 when unsupported.
|
|
|
|
// Whether this arena locks and unlocks its own pages as it commits and decommits them.
|
|
//
|
|
// Large pages are non-pageable by construction, so a .Secure_Arena that got them already holds
|
|
// the residency guarantee lock_range exists to provide. Locking them again would at best be a
|
|
// wasted syscall on the allocation path and at worst a spurious .Secure_Lock_Failed -- and the
|
|
// unlock paths have to agree, or they would unlock a range that was never locked.
|
|
arena_manages_page_locks :: (arena: *Arena) -> bool #expand {
|
|
return arena_is_secure(arena) && !arena_large_pages_active(arena);
|
|
}
|
|
|
|
// Whether this arena should try for large pages, and at what granularity. Only what the OS call
|
|
// itself requires is checked: there is deliberately no ceiling on the reserve class, so a caller
|
|
// asking for large pages on a huge reservation gets what it asked for on the platforms that
|
|
// commit the whole thing.
|
|
large_page_granularity_for :: (arena: *Arena, size: s64) -> (page_size: s64, eligible: bool) {
|
|
if !arena_is_large_pages(arena) return 0, false;
|
|
|
|
large := platform_large_page_size();
|
|
if large <= 0 || (large & (large - 1)) != 0 {
|
|
// Every real large page size is a power of two; refusing a reported non-power-of-two keeps
|
|
// the log2 that all the page arithmetic runs on well defined.
|
|
log_warning("[Basic/Arena] .Large_Pages was requested but this machine reports no usable large page size (got %). Using % byte pages.",
|
|
large, PLATFORM_MEMORY_PAGE_SIZE);
|
|
return 0, false;
|
|
}
|
|
|
|
if size < large || (size % large) != 0 {
|
|
log_warning("[Basic/Arena] .Large_Pages was requested for a % byte reservation, which is not a whole multiple of the % byte large page size. Using % byte pages.",
|
|
size, large, PLATFORM_MEMORY_PAGE_SIZE);
|
|
return 0, false;
|
|
}
|
|
|
|
return large, true;
|
|
}
|
|
|
|
log2_of_power_of_two :: (value: s64) -> u8 {
|
|
assert(value > 0 && (value & (value - 1)) == 0);
|
|
|
|
result: u8 = 0;
|
|
remaining := value;
|
|
while remaining != 1 {
|
|
remaining >>= 1;
|
|
result += 1;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
ceil_log2 :: (value: s64) -> u8 {
|
|
assert(value >= 1);
|
|
|
|
result: u8 = 0;
|
|
while (cast(s64) 1 << result) < value result += 1;
|
|
return result;
|
|
}
|
|
|
|
#scope_export
|
|
|
|
#if OS == .WINDOWS {
|
|
platform_large_page_size :: () -> s64 {
|
|
// Reports what the CPU supports, independently of whether the process holds
|
|
// SeLockMemoryPrivilege. Without the privilege the VirtualAlloc below simply fails, which
|
|
// the fallback already handles, and granting it is the main program's job.
|
|
if cached_large_page_size == UNQUERIED cached_large_page_size = cast(s64) GetLargePageMinimum();
|
|
return cached_large_page_size;
|
|
}
|
|
|
|
arena_platform_init :: (using arena: *Arena, new_reserve: Arena_Reserve) -> bool {
|
|
reserve = new_reserve;
|
|
size := reserve_size_bytes(new_reserve);
|
|
page_size_log2 = PLATFORM_MEMORY_PAGE_SIZE_LOG2;
|
|
|
|
large_page_size, eligible := large_page_granularity_for(arena, size);
|
|
if eligible {
|
|
// MEM_LARGE_PAGES has no reserve-only form, so this one call commits and wires all `size`
|
|
// bytes. That is why first_uncommitted_page starts at the top: there is nothing left for
|
|
// extend_committed_pages to commit, and MEM_DECOMMIT cannot take part of it back.
|
|
p := VirtualAlloc(null, cast(u64) size, MEM_RESERVE | MEM_COMMIT | MEM_LARGE_PAGES, PAGE_READWRITE);
|
|
if p {
|
|
memory_base = p;
|
|
page_size_log2 = log2_of_power_of_two(large_page_size);
|
|
first_uncommitted_page = memory_base + size;
|
|
return true;
|
|
}
|
|
|
|
// Usually a missing SeLockMemoryPrivilege, or no free physical run of `size` contiguous
|
|
// bytes. Neither is fatal: the 4 KB reservation below is a working arena.
|
|
value, description := System.get_error_value_and_string();
|
|
log_warning("[Basic/Arena] Large page VirtualAlloc of % bytes failed with code %: %. Falling back to % byte pages.",
|
|
size, value, description, PLATFORM_MEMORY_PAGE_SIZE);
|
|
}
|
|
if arena_is_large_pages(arena) status |= .Large_Pages_Failed;
|
|
|
|
memory_base = VirtualAlloc(null, cast(u64) size, MEM_RESERVE, PAGE_READWRITE);
|
|
success := memory_base != null;
|
|
if !success {
|
|
value, description := System.get_error_value_and_string();
|
|
log_error("[Basic/Arena] In arena_platform_init, VirtualAlloc failed with code %: %\n", value, description);
|
|
return false;
|
|
}
|
|
|
|
first_uncommitted_page = memory_base;
|
|
return true;
|
|
}
|
|
|
|
// A fully committed large-page arena never reaches the commit call below: its
|
|
// first_uncommitted_page already sits at the top of the reservation, so `delta` cannot be
|
|
// positive and this returns at the guard.
|
|
extend_committed_pages :: (arena: *Arena, end: *u8) -> bool {
|
|
delta := cast(s64)(end - arena.first_uncommitted_page);
|
|
if delta <= 0 return true;
|
|
size := Align(delta, arena_page_size(arena));
|
|
|
|
p := VirtualAlloc(arena.first_uncommitted_page, cast(u64)size, MEM_COMMIT, PAGE_READWRITE);
|
|
if !p {
|
|
value, description := System.get_error_value_and_string();
|
|
log_error("[Basic/Arena] In extend_committed_pages, VirtualAlloc failed with code %: %\n", value, description);
|
|
return false;
|
|
}
|
|
|
|
if arena_manages_page_locks(arena) {
|
|
if !lock_range(arena.first_uncommitted_page, size) arena.status |= .Secure_Lock_Failed;
|
|
}
|
|
|
|
arena.first_uncommitted_page += size;
|
|
return true;
|
|
}
|
|
|
|
#scope_module
|
|
cached_large_page_size: s64 = UNQUERIED;
|
|
lock_range :: (start: *u8, byte_count: s64) -> bool {
|
|
if byte_count <= 0 return true;
|
|
if cast(bool) VirtualLock(start, cast(u64)byte_count) return true;
|
|
|
|
value, description := System.get_error_value_and_string();
|
|
log_error("[Basic/Arena] VirtualLock of % bytes failed with code %: %\n", byte_count, value, description);
|
|
return false;
|
|
}
|
|
unlock_range :: (start: *u8, byte_count: s64) -> bool {
|
|
if byte_count <= 0 return true;
|
|
if cast(bool) VirtualUnlock(start, cast(u64)byte_count) return true;
|
|
|
|
value, description := System.get_error_value_and_string();
|
|
log_error("[Basic/Arena] VirtualUnlock of % bytes failed with code %: %\n", byte_count, value, description);
|
|
return false;
|
|
}
|
|
free_pages_down_to :: (arena: *Arena, pages_to_keep: s64) {
|
|
if (arena == null) return;
|
|
if arena_is_bootstrapped(arena) assert(pages_to_keep >= 1, "Always keep at least 1 page if we're bootstrapping arena");
|
|
|
|
// A large-page allocation is released as a whole or not at all -- there is no partial
|
|
// MEM_DECOMMIT for one -- so such an arena rewinds but never drops below its reservation.
|
|
#if LARGE_PAGES_ARE_FULLY_COMMITTED {
|
|
if arena_large_pages_active(arena) return;
|
|
}
|
|
|
|
bytes_to_keep := pages_to_keep * arena_page_size(arena);
|
|
assert(bytes_to_keep <= reserve_size_bytes(arena), "Requesting to keep more bytes than there is address space!");
|
|
|
|
bytes_committed := cast(s64)(arena.first_uncommitted_page - arena.memory_base);
|
|
if (bytes_to_keep > bytes_committed) {
|
|
return; // nothing to do
|
|
}
|
|
|
|
start_address := arena.memory_base + bytes_to_keep;
|
|
bytes_to_decommit := cast(u64)(arena.first_uncommitted_page - start_address);
|
|
|
|
// `VirtualFree(start_address, 0, MEM_DECOMMIT)` decommits the entire region:
|
|
if bytes_to_decommit {
|
|
// Decommitting drops the lock on these pages anyway; unlocking first keeps the process's
|
|
// locked-page accounting honest, so the next commit is not refused against a stale total.
|
|
if arena_manages_page_locks(arena) unlock_range(start_address, cast(s64)bytes_to_decommit);
|
|
|
|
VirtualFree(start_address, bytes_to_decommit, MEM_DECOMMIT);
|
|
|
|
arena.first_uncommitted_page = start_address;
|
|
}
|
|
}
|
|
#scope_export
|
|
arena_lock_pages :: (arena: *Arena) -> bool {
|
|
if !is_valid(arena) return false;
|
|
|
|
// Large pages are already non-pageable, so the guarantee this provides is one the arena
|
|
// holds either way. Reporting success without the syscall keeps it paired with the unlock.
|
|
if arena_large_pages_active(arena) return true;
|
|
|
|
byte_count := arena_committed_bytes(arena);
|
|
if byte_count == 0 {
|
|
log_error("Attempting to lock pages in an arena with no active pages!");
|
|
return false;
|
|
}
|
|
|
|
return lock_range(arena.memory_base, byte_count);
|
|
}
|
|
|
|
arena_unlock_pages :: (arena: *Arena) -> bool {
|
|
if !is_valid(arena) return false;
|
|
if arena_large_pages_active(arena) return true; // Nothing was locked; see arena_lock_pages.
|
|
|
|
byte_count := arena_committed_bytes(arena);
|
|
if byte_count == 0 return true;
|
|
|
|
return unlock_range(arena.memory_base, byte_count);
|
|
}
|
|
} else #if OS_IS_UNIX {
|
|
P :: #import "POSIX"; // Can remove this dependency since we only use a few things.
|
|
|
|
platform_large_page_size :: () -> s64 {
|
|
if cached_large_page_size != UNQUERIED return cached_large_page_size;
|
|
|
|
#if OS == .MACOS {
|
|
// Darwin superpages are an x86_64-only facility; Apple Silicon exposes no user-facing
|
|
// equivalent, so there is nothing to report there.
|
|
#if CPU == .X64 {
|
|
cached_large_page_size = 2 * 1024 * 1024;
|
|
} else {
|
|
cached_large_page_size = 0;
|
|
}
|
|
} else {
|
|
cached_large_page_size = linux_default_huge_page_size();
|
|
}
|
|
|
|
return cached_large_page_size;
|
|
}
|
|
|
|
arena_platform_init :: (using arena: *Arena, new_reserve: Arena_Reserve) -> bool {
|
|
reserve = new_reserve;
|
|
size := reserve_size_bytes(new_reserve);
|
|
page_size_log2 = PLATFORM_MEMORY_PAGE_SIZE_LOG2;
|
|
|
|
PROT :: P.PROT_READ | P.PROT_WRITE;
|
|
FLAGS :: P.MAP_PRIVATE | P.MAP_ANONYMOUS;
|
|
|
|
large_page_size, eligible := large_page_granularity_for(arena, size);
|
|
if eligible {
|
|
mapped: *void = P.MAP_FAILED;
|
|
|
|
#if OS == .MACOS {
|
|
// There is no mmap flag for superpages; the size class travels in the fd argument.
|
|
// Wired and fully committed at map time, the way Windows large pages are.
|
|
mapped = P.mmap(null, cast(u64) size, PROT, FLAGS, VM_FLAGS_SUPERPAGE_SIZE_2MB, 0);
|
|
} else {
|
|
// MAP_HUGETLB draws from the hugetlbfs pool an administrator preallocated with
|
|
// vm.nr_hugepages, and returns a huge-page-aligned address. It is the only form that
|
|
// actually guarantees the granularity, so it is what gets tried.
|
|
mapped = P.mmap(null, cast(u64) size, PROT, FLAGS | P.MAP_HUGETLB, 0, 0);
|
|
}
|
|
|
|
if mapped != P.MAP_FAILED {
|
|
memory_base = mapped;
|
|
page_size_log2 = log2_of_power_of_two(large_page_size);
|
|
#if LARGE_PAGES_ARE_FULLY_COMMITTED {
|
|
first_uncommitted_page = memory_base + size; // Wired in full at map time; nothing left to commit.
|
|
} else {
|
|
first_uncommitted_page = memory_base;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// On Linux this is the common case: no preallocated hugetlbfs pool.
|
|
value, text := System.get_error_value_and_string();
|
|
log_warning("[Basic/Arena] Large page mmap of % bytes failed with code %: %. Falling back to % byte pages.",
|
|
size, value, text, PLATFORM_MEMORY_PAGE_SIZE);
|
|
}
|
|
if arena_is_large_pages(arena) status |= .Large_Pages_Failed;
|
|
|
|
memory_base = P.mmap(null, cast(u64) size, PROT, FLAGS, 0, 0);
|
|
success := memory_base != P.MAP_FAILED;
|
|
if !success {
|
|
value, text := System.get_error_value_and_string();
|
|
log_error("[Basic/Arena] In Arena, mmap failed with code %: %\n", value, text);
|
|
memory_base = null;
|
|
return false;
|
|
}
|
|
|
|
#if OS == .LINUX {
|
|
// Transparent huge pages: strictly advisory. The kernel may back the range with huge pages
|
|
// and may split them again, so the arena keeps 4 KB arithmetic and still reports
|
|
// arena_large_pages_active as false. It costs one syscall and may recover most of the TLB
|
|
// win the hugetlbfs pool would have given, so a caller that asked gets it anyway.
|
|
if arena_is_large_pages(arena) P.madvise(memory_base, cast(u64) size, P.MADV_HUGEPAGE);
|
|
}
|
|
|
|
first_uncommitted_page = memory_base;
|
|
return true;
|
|
}
|
|
|
|
extend_committed_pages :: (arena: *Arena, end: *u8) -> bool {
|
|
delta := end - arena.first_uncommitted_page;
|
|
if delta <= 0 return true;
|
|
size := Align(delta, arena_page_size(arena));
|
|
|
|
// mlock forces the pages resident immediately rather than on first touch.
|
|
//
|
|
// Recorded rather than propagated, for the reason given in the Windows arm: a low
|
|
// RLIMIT_MEMLOCK must not become an out-of-memory.
|
|
if arena_manages_page_locks(arena) {
|
|
if !lock_range(arena.first_uncommitted_page, size) arena.status |= .Secure_Lock_Failed;
|
|
}
|
|
|
|
arena.first_uncommitted_page += size;
|
|
|
|
return true;
|
|
}
|
|
|
|
#scope_module
|
|
cached_large_page_size: s64 = UNQUERIED;
|
|
|
|
#if OS != .MACOS {
|
|
// `Hugepagesize:` in /proc/meminfo is the size MAP_HUGETLB maps at when no explicit size is
|
|
// requested. Basic cannot import File -- File imports Basic -- so this goes through libc
|
|
// directly, in the style of the nested bindings in posix.jai.
|
|
linux_default_huge_page_size :: () -> s64 {
|
|
FALLBACK :: 2 * 1024 * 1024; // What x86_64 and 4 KB-granule arm64 both default to.
|
|
KEY :: "Hugepagesize:";
|
|
|
|
fd := P.open("/proc/meminfo".data, P.O_RDONLY);
|
|
if fd < 0 return FALLBACK;
|
|
defer P.close(fd);
|
|
|
|
// /proc/meminfo reports a size of 0, so it has to be read rather than stat'd. The key sits
|
|
// in the first few hundred bytes in practice; one page is a generous margin.
|
|
buffer: [4096] u8 = ---;
|
|
bytes_read := P.read(fd, buffer.data, cast(u64) buffer.count);
|
|
if bytes_read <= 0 return FALLBACK;
|
|
|
|
at := 0;
|
|
while at + KEY.count <= bytes_read {
|
|
matched := true;
|
|
for i: 0 .. KEY.count - 1 {
|
|
if buffer[at + i] != KEY[i] {
|
|
matched = false;
|
|
break;
|
|
}
|
|
}
|
|
if matched break;
|
|
at += 1;
|
|
}
|
|
if at + KEY.count > bytes_read return FALLBACK;
|
|
|
|
cursor := at + KEY.count;
|
|
while cursor < bytes_read && buffer[cursor] == #char " " cursor += 1;
|
|
|
|
kilobytes: s64 = 0; // The file states its own unit, and it is always kB for this key.
|
|
while cursor < bytes_read && buffer[cursor] >= #char "0" && buffer[cursor] <= #char "9" {
|
|
kilobytes = kilobytes * 10 + cast(s64)(buffer[cursor] - #char "0");
|
|
cursor += 1;
|
|
}
|
|
|
|
if kilobytes <= 0 return FALLBACK;
|
|
return kilobytes * 1024;
|
|
}
|
|
}
|
|
|
|
lock_range :: (start: *u8, byte_count: s64) -> bool {
|
|
if byte_count <= 0 return true;
|
|
if P.mlock(start, cast(u64)byte_count) == 0 return true;
|
|
|
|
value, description := System.get_error_value_and_string();
|
|
log_error("[Basic/Arena] mlock of % bytes failed with code %: %\n", byte_count, value, description);
|
|
return false;
|
|
}
|
|
|
|
unlock_range :: (start: *u8, byte_count: s64) -> bool {
|
|
if byte_count <= 0 return true;
|
|
if P.munlock(start, cast(u64)byte_count) == 0 return true;
|
|
|
|
value, description := System.get_error_value_and_string();
|
|
log_error("[Basic/Arena] munlock of % bytes failed with code %: %\n", byte_count, value, description);
|
|
return false;
|
|
}
|
|
free_pages_down_to :: (using arena: *Arena, pages_to_keep: s64) {
|
|
if (arena == null) return;
|
|
if arena_is_bootstrapped(arena) assert(pages_to_keep >= 1, "Always keep at least 1 page if we're bootstrapping arena");
|
|
|
|
// Darwin superpages are wired at map time and cannot be partially released; Linux hugetlb
|
|
// pages can, at huge-page granularity, which page_size_log2 already accounts for.
|
|
#if LARGE_PAGES_ARE_FULLY_COMMITTED {
|
|
if arena_large_pages_active(arena) return;
|
|
}
|
|
|
|
bytes_to_keep := pages_to_keep * arena_page_size(arena);
|
|
assert(bytes_to_keep <= reserve_size_bytes(arena), "Requesting to keep more bytes than there is address space!");
|
|
|
|
bytes_committed := cast(s64)(arena.first_uncommitted_page - arena.memory_base);
|
|
if (bytes_to_keep > bytes_committed) {
|
|
return; // nothing to do
|
|
}
|
|
|
|
start_address := arena.memory_base + bytes_to_keep;
|
|
bytes_to_decommit := cast(u64)(arena.first_uncommitted_page - start_address);
|
|
if bytes_to_decommit {
|
|
if arena_manages_page_locks(arena) unlock_range(start_address, cast(s64)bytes_to_decommit);
|
|
|
|
if P.madvise(start_address, bytes_to_decommit, P.MADV_DONTNEED) != 0 {
|
|
value, description := System.get_error_value_and_string();
|
|
log_error("[Basic/Arena] In free_pages_down_to, madvise failed with code %: %\n", value, description);
|
|
}
|
|
arena.first_uncommitted_page = start_address;
|
|
}
|
|
}
|
|
#scope_export
|
|
arena_lock_pages :: (arena: *Arena) -> bool {
|
|
if !is_valid(arena) return false;
|
|
|
|
// Large pages are already non-pageable, so the guarantee this provides is one the arena
|
|
// holds either way. Reporting success without the syscall keeps it paired with the unlock.
|
|
if arena_large_pages_active(arena) return true;
|
|
|
|
byte_count := arena_committed_bytes(arena);
|
|
if byte_count == 0 {
|
|
log_error("Attempting to lock pages in an arena with no active pages!");
|
|
return false;
|
|
}
|
|
|
|
return lock_range(arena.memory_base, byte_count);
|
|
}
|
|
|
|
arena_unlock_pages :: (arena: *Arena) -> bool {
|
|
if !is_valid(arena) return false;
|
|
if arena_large_pages_active(arena) return true; // Nothing was locked; see arena_lock_pages.
|
|
|
|
byte_count := arena_committed_bytes(arena);
|
|
if byte_count == 0 return true;
|
|
|
|
return unlock_range(arena.memory_base, byte_count);
|
|
}
|
|
}
|
|
|
|
Align :: inline (n: $T, $$ alignment: s64) -> T {
|
|
assert(alignment > 0 && (alignment & (alignment - 1)) == 0);
|
|
return (n + alignment-1) & ~(alignment-1);
|
|
}
|
|
|
|
System :: #import "System";
|
|
|
|
#scope_file
|
|
#if OS == .WINDOWS {
|
|
kernel32 :: #library,system "kernel32";
|
|
VirtualAlloc :: (lpAddress: *void, dwSize: u64, flAllocationType: u32, flProtect: u32) -> *void #foreign kernel32;
|
|
VirtualFree :: (lpAddress: *void, dwSize: u64, dwFreeType: u32) -> BOOL #foreign kernel32;
|
|
VirtualLock :: (lpAddress: *void, dwSize: u64) -> BOOL #foreign kernel32;
|
|
VirtualUnlock :: (lpAddress: *void, dwSize: u64) -> BOOL #foreign kernel32;
|
|
GetLargePageMinimum :: () -> u64 #foreign kernel32;
|
|
PAGE_READWRITE :: 0x04;
|
|
MEM_COMMIT :: 0x00001000;
|
|
MEM_RESERVE :: 0x00002000;
|
|
MEM_DECOMMIT :: 0x00004000;
|
|
MEM_RELEASE :: 0x00008000;
|
|
MEM_LARGE_PAGES :: 0x20000000;
|
|
BOOL :: enum s32 { FALSE :: 0; TRUE :: 1; }
|
|
}
|
|
else #if OS == .MACOS {
|
|
// From macOS/bindings/mach.jai. mmap takes these in its fd argument rather than its flags.
|
|
SUPERPAGE_SIZE_2MB :: 2;
|
|
VM_FLAGS_SUPERPAGE_SHIFT :: 16;
|
|
VM_FLAGS_SUPERPAGE_SIZE_2MB :: SUPERPAGE_SIZE_2MB << VM_FLAGS_SUPERPAGE_SHIFT;
|
|
}
|