|
| 1 | +#+private |
| 2 | + |
| 3 | +package page_allocator |
| 4 | + |
| 5 | +import "base:runtime" |
| 6 | +import "base:intrinsics" |
| 7 | + |
| 8 | +import "core:mem" |
| 9 | +import "core:mem/virtual" |
| 10 | + |
| 11 | +import "core:sys/linux" |
| 12 | + |
| 13 | +_GRANULARITY_MIN :: 4 * mem.Kilobyte |
| 14 | +_GRANULARITY_MAX :: 1 * mem.Gigabyte |
| 15 | + |
| 16 | +// log2 of the huge page size << 26 |
| 17 | +MAP_HUGE_2MB : i32 : 21 << 26 |
| 18 | +MAP_HUGE_1GB : i32 : 30 << 26 |
| 19 | + |
| 20 | +@(require_results) |
| 21 | +_page_aligned_alloc :: proc(size, alignment, granularity: int, |
| 22 | + flags: Page_Allocator_Flags) -> (memory: []byte, err: mem.Allocator_Error) { |
| 23 | + seek_alignment_and_commit :: proc(reserve: []byte, size, alignment, granularity: int) -> (memory: []byte, err: mem.Allocator_Error) { |
| 24 | + size_full := mem.align_forward_int(size, granularity) |
| 25 | + base := uintptr(&reserve[0]) |
| 26 | + addr := mem.align_forward_int(int(base), alignment) |
| 27 | + |
| 28 | + base_waste := addr - int(base) |
| 29 | + memory = reserve[base_waste:base_waste + size] |
| 30 | + |
| 31 | + if base_waste > 0 { |
| 32 | + virtual.release(&reserve[0], uint(base_waste)) |
| 33 | + } |
| 34 | + |
| 35 | + post_waste := (alignment - granularity) - base_waste |
| 36 | + if post_waste > 0 { |
| 37 | + post_waste_ptr := rawptr(uintptr(addr + size_full)) |
| 38 | + virtual.release(post_waste_ptr, uint(post_waste)) |
| 39 | + } |
| 40 | + |
| 41 | + err = virtual.commit(&memory[0], uint(size_full)) |
| 42 | + if err != nil { |
| 43 | + virtual.release(&memory[0], uint(size_full)) |
| 44 | + memory = nil |
| 45 | + } |
| 46 | + return |
| 47 | + } |
| 48 | + |
| 49 | + assert(granularity >= GRANULARITY_MIN) |
| 50 | + assert(granularity <= GRANULARITY_MAX) |
| 51 | + assert(runtime.is_power_of_two(granularity)) |
| 52 | + assert(alignment <= GRANULARITY_MAX) |
| 53 | + assert(runtime.is_power_of_two(alignment)) |
| 54 | + if size == 0 { |
| 55 | + return nil, .Invalid_Argument |
| 56 | + } |
| 57 | + |
| 58 | + if .Allow_Large_Pages in flags && granularity >= mem.Gigabyte && size >= mem.Gigabyte { |
| 59 | + raw_map_flags := i32(MAP_HUGE_1GB) |
| 60 | + map_flags := transmute(linux.Map_Flags)(raw_map_flags) |
| 61 | + map_flags += {.ANONYMOUS, .PRIVATE, .HUGETLB} |
| 62 | + |
| 63 | + size_full := uint(mem.align_forward_int(size, granularity)) |
| 64 | + ptr, errno := linux.mmap(0, uint(size_full), {.READ, .WRITE}, map_flags) |
| 65 | + if ptr != nil && errno == nil { |
| 66 | + return mem.byte_slice(ptr, size), nil |
| 67 | + } |
| 68 | + } |
| 69 | + |
| 70 | + alignment := alignment |
| 71 | + alignment = max(alignment, GRANULARITY_MIN) |
| 72 | + if granularity == GRANULARITY_MIN && alignment == GRANULARITY_MIN { |
| 73 | + size_full := mem.align_forward_int(size, GRANULARITY_MIN) |
| 74 | + memory, err = virtual.reserve_and_commit(uint(size_full)) |
| 75 | + if err != nil { |
| 76 | + memory = memory[:size] |
| 77 | + } |
| 78 | + return |
| 79 | + } |
| 80 | + |
| 81 | + huge_if: if .Allow_Large_Pages in flags && granularity > 2 * mem.Megabyte && size > 2 * mem.Megabyte { |
| 82 | + raw_map_flags := i32(MAP_HUGE_2MB) |
| 83 | + map_flags := transmute(linux.Map_Flags)(raw_map_flags) |
| 84 | + map_flags += {.ANONYMOUS, .PRIVATE, .HUGETLB} |
| 85 | + |
| 86 | + size_full := mem.align_forward_int(size, granularity) |
| 87 | + if alignment < 2 * mem.Megabyte { |
| 88 | + ptr, errno := linux.mmap(0, uint(size_full), {.READ, .WRITE}, map_flags) |
| 89 | + if ptr != nil && errno == nil { |
| 90 | + return mem.byte_slice(ptr, size), nil |
| 91 | + } |
| 92 | + } else { |
| 93 | + reserve_size := size_full + (alignment - 2 * mem.Megabyte) |
| 94 | + reserve_ptr, errno := linux.mmap(0, uint(size_full), {}, map_flags) |
| 95 | + reserve := mem.byte_slice(reserve_ptr, reserve_size) |
| 96 | + if reserve_ptr != nil && errno == nil { |
| 97 | + return seek_alignment_and_commit(reserve, size, alignment, granularity) |
| 98 | + } |
| 99 | + } |
| 100 | + } |
| 101 | + |
| 102 | + // We gave ourselves enough extra space to retrieve the size AND seek |
| 103 | + // to the desired alignment. |
| 104 | + size_full := mem.align_forward_int(size, granularity) |
| 105 | + reserve_size := size_full + (alignment - GRANULARITY_MIN) |
| 106 | + reserve := virtual.reserve(uint(reserve_size)) or_return |
| 107 | + return seek_alignment_and_commit(reserve, size, alignment, granularity) |
| 108 | +} |
| 109 | + |
| 110 | + |
| 111 | +@(require_results) |
| 112 | +_page_aligned_resize :: proc(old_ptr: rawptr, |
| 113 | + old_size, new_size, new_alignment, granularity: int, |
| 114 | + flags: Page_Allocator_Flags) -> (memory: []byte, err: mem.Allocator_Error) { |
| 115 | + assert(granularity >= GRANULARITY_MIN) |
| 116 | + assert(granularity <= GRANULARITY_MAX) |
| 117 | + assert(runtime.is_power_of_two(granularity)) |
| 118 | + assert(new_alignment <= GRANULARITY_MAX) |
| 119 | + assert(runtime.is_power_of_two(new_alignment)) |
| 120 | + |
| 121 | + if old_ptr == nil || !mem.is_aligned(old_ptr, granularity) { |
| 122 | + return mem.byte_slice(old_ptr, old_size), .Invalid_Pointer |
| 123 | + } |
| 124 | + if new_size == 0 { |
| 125 | + return nil, page_free(old_ptr, old_size) |
| 126 | + } |
| 127 | + |
| 128 | + old_size_full := mem.align_forward_int(old_size, granularity) |
| 129 | + new_size_full := mem.align_forward_int(new_size, granularity) |
| 130 | + |
| 131 | + new_alignment := new_alignment |
| 132 | + new_alignment = max(new_alignment, GRANULARITY_MIN) |
| 133 | + |
| 134 | + // Can we meet the request with existing memory? |
| 135 | + if new_size <= old_size_full { |
| 136 | + memory = mem.byte_slice(old_ptr, new_size) |
| 137 | + if new_size > old_size { |
| 138 | + if .Uninitialized_Memory not_in flags { |
| 139 | + mem.zero_slice(memory[old_size:]) |
| 140 | + } |
| 141 | + } else if new_size_full < old_size_full { |
| 142 | + unused := uintptr(old_ptr) + uintptr(new_size_full) |
| 143 | + virtual.release(rawptr(unused), uint(old_size_full - new_size_full)) |
| 144 | + } |
| 145 | + return |
| 146 | + } |
| 147 | + |
| 148 | + // Can we resize in place? |
| 149 | + memory, err = _resize_allocation(old_ptr, uint(old_size_full), uint(new_size_full), may_move=false) |
| 150 | + if err == nil { |
| 151 | + return memory[:new_size], nil |
| 152 | + } |
| 153 | + |
| 154 | + // Do we *need* to do a manual allocate -> copy -> free? |
| 155 | + if .Never_Free in flags || new_alignment > GRANULARITY_MIN { |
| 156 | + memory, err = page_aligned_alloc(new_size, new_alignment, granularity, flags) |
| 157 | + if err != nil { |
| 158 | + return mem.byte_slice(old_ptr, old_size), err |
| 159 | + } |
| 160 | + |
| 161 | + mem.copy_non_overlapping(&memory[0], old_ptr, old_size) |
| 162 | + if .Never_Free not_in flags { |
| 163 | + virtual.release(old_ptr, uint(old_size_full)) |
| 164 | + } |
| 165 | + return |
| 166 | + } |
| 167 | + |
| 168 | + memory, err = _resize_allocation(old_ptr, uint(old_size_full), uint(new_size_full), may_move=true) |
| 169 | + if err != nil { |
| 170 | + return mem.byte_slice(old_ptr, old_size), err |
| 171 | + } |
| 172 | + memory = memory[:new_size] |
| 173 | + |
| 174 | + if .Allow_Large_Pages in flags && |
| 175 | + new_size > 2 * mem.Megabyte && |
| 176 | + new_size - old_size > new_size / 2 { |
| 177 | + attempt_huge_page_collapse(&memory[0], len(memory)) |
| 178 | + } |
| 179 | + |
| 180 | + return |
| 181 | +} |
| 182 | + |
| 183 | +_resize_allocation :: proc (old_data: rawptr, old_size, new_size: uint, may_move: bool) -> (data: []byte, err: mem.Allocator_Error) { |
| 184 | + flags: linux.MRemap_Flags |
| 185 | + if may_move { |
| 186 | + flags += {.MAYMOVE} |
| 187 | + } |
| 188 | + |
| 189 | + addr, errno := linux.mremap(old_data, old_size, new_size, flags) |
| 190 | + #partial switch errno { |
| 191 | + case .EINVAL, .EFAULT: |
| 192 | + return (cast([^]byte)old_data)[:old_size], .Invalid_Pointer |
| 193 | + case .ENOMEM, .EAGAIN: |
| 194 | + return (cast([^]byte)old_data)[:old_size], .Out_Of_Memory |
| 195 | + } |
| 196 | + return (cast([^]byte)addr)[:new_size], nil |
| 197 | +} |
| 198 | + |
| 199 | +attempt_huge_page_collapse :: proc(addr: rawptr, size: int) { |
| 200 | + if size < 2 * mem.Megabyte { |
| 201 | + return |
| 202 | + } |
| 203 | + |
| 204 | + size_full := mem.align_forward_int(size, 2 * mem.Megabyte) |
| 205 | + huge_page_addr := mem.align_forward(addr, 2 * mem.Megabyte) |
| 206 | + huge_page_size := uintptr(size_full) - (uintptr(huge_page_addr) - uintptr(addr)) |
| 207 | + if huge_page_size < 2 * mem.Megabyte { |
| 208 | + return |
| 209 | + } |
| 210 | + |
| 211 | + // This is purely an optimization that attempts to use Transparent Huge Pages (THPs). |
| 212 | + // THPs have the same semantics as regular 4K pages, so we don't need to track them. |
| 213 | + _ = linux.madvise(huge_page_addr, uint(huge_page_size), .COLLAPSE) |
| 214 | +} |
0 commit comments