Skip to content

Commit 07d8a82

Browse files
committed
first
0 parents  commit 07d8a82

File tree

4 files changed

+555
-0
lines changed

4 files changed

+555
-0
lines changed

page_allocator/page_allocator.odin

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
package page_allocator
2+
3+
import "base:runtime"
4+
import "base:intrinsics"
5+
6+
import "core:mem"
7+
import "core:mem/virtual"
8+
9+
// The idea behind the Page_Allocator is to have a low-level lookup-less allocator
10+
// over the operating system functions. We can use the provided size to infer the
11+
// actual page aligned size. We can also *pretend* to have a different page size by
12+
// setting the granularity. This provides greater control over alignment while still
13+
// being able to infer the correct size of the allocation.
14+
//
15+
// NOTE: Because the size is being inferred, allocations that come from new, must
16+
// provide size with something like mem.free_with_size to release the memory.
17+
18+
Page_Allocator_Flag :: enum {
19+
Never_Free,
20+
Allow_Large_Pages,
21+
Uninitialized_Memory,
22+
No_Commit, // TODO
23+
}
24+
Page_Allocator_Flags :: bit_set[Page_Allocator_Flag; u8]
25+
26+
Page_Allocator :: struct {
27+
flags: Page_Allocator_Flags,
28+
granularity_log2: u8,
29+
}
30+
31+
GRANULARITY_MIN :: _GRANULARITY_MIN
32+
GRANULARITY_MAX :: _GRANULARITY_MAX
33+
34+
@(require_results)
35+
page_aligned_alloc :: proc(size: int,
36+
alignment: int = GRANULARITY_MIN,
37+
granularity: int = GRANULARITY_MIN,
38+
flags: Page_Allocator_Flags = {}) -> (memory: []byte, err: mem.Allocator_Error) {
39+
return _page_aligned_alloc(size, alignment, granularity, flags)
40+
}
41+
42+
@(require_results)
43+
page_aligned_resize :: proc(old_ptr: rawptr,
44+
old_size, new_size: int,
45+
new_alignment: int = GRANULARITY_MIN,
46+
granularity: int = GRANULARITY_MIN,
47+
flags: Page_Allocator_Flags = {}) -> (memory: []byte, err: mem.Allocator_Error) {
48+
return _page_aligned_resize(old_ptr, old_size, new_size, new_alignment, granularity, flags)
49+
}
50+
51+
page_free :: proc(p: rawptr, size: int, granularity := GRANULARITY_MIN, flags: Page_Allocator_Flags = {}) -> mem.Allocator_Error {
52+
assert(granularity >= GRANULARITY_MIN)
53+
assert(runtime.is_power_of_two(granularity))
54+
if p == nil || !mem.is_aligned(p, granularity) {
55+
return .Invalid_Pointer
56+
}
57+
if size <= 0 {
58+
// NOTE: This would actually work fine on Windows, but we'd need to track
59+
// allocations on every other system.
60+
unimplemented("Page allocator does not track size. Try mem.free_with_size().")
61+
}
62+
size_full := mem.align_forward_int(size, granularity)
63+
virtual.release(p, uint(size_full))
64+
return nil
65+
}
66+
67+
@(require_results)
68+
page_allocator_make :: proc(granularity := GRANULARITY_MIN, flags: Page_Allocator_Flags = {}) -> Page_Allocator {
69+
assert(granularity >= GRANULARITY_MIN)
70+
assert(granularity <= GRANULARITY_MAX)
71+
assert(runtime.is_power_of_two(granularity))
72+
return Page_Allocator {
73+
flags = flags,
74+
granularity_log2 = u8(intrinsics.count_trailing_zeros(granularity)),
75+
}
76+
}
77+
78+
// The Page_Allocator does not track individual allocations, so the struct itself
79+
// only contains configuration. If the data is nil, we will just use the defaults.
80+
@(require_results)
81+
page_allocator :: proc(allocator: ^Page_Allocator = nil)-> mem.Allocator {
82+
return mem.Allocator {
83+
procedure = page_allocator_proc,
84+
data = allocator,
85+
}
86+
}
87+
88+
page_allocator_proc :: proc(allocator_data: rawptr, mode: mem.Allocator_Mode,
89+
size, alignment: int,
90+
old_memory: rawptr, old_size: int, loc := #caller_location) -> ([]byte, mem.Allocator_Error) {
91+
flags: Page_Allocator_Flags
92+
granularity := GRANULARITY_MIN
93+
94+
if allocator := (^Page_Allocator)(allocator_data); allocator != nil {
95+
flags = allocator.flags
96+
if allocator.granularity_log2 != 0 {
97+
granularity = 1 << allocator.granularity_log2
98+
}
99+
}
100+
101+
switch mode {
102+
case .Alloc_Non_Zeroed:
103+
flags += {.Uninitialized_Memory}
104+
return page_aligned_alloc(size, alignment, granularity, flags)
105+
106+
case .Alloc:
107+
flags -= {.Uninitialized_Memory}
108+
return page_aligned_alloc(size, alignment, granularity, flags)
109+
110+
case .Free:
111+
return nil, page_free(old_memory, old_size, granularity, flags)
112+
113+
case .Free_All:
114+
return nil, .Mode_Not_Implemented
115+
116+
case .Resize_Non_Zeroed:
117+
flags += {.Uninitialized_Memory}
118+
break
119+
120+
case .Resize:
121+
flags -= {.Uninitialized_Memory}
122+
break
123+
124+
case .Query_Features:
125+
set := (^mem.Allocator_Mode_Set)(old_memory)
126+
if set != nil {
127+
set^ = {.Alloc, .Alloc_Non_Zeroed, .Free, .Resize, .Resize_Non_Zeroed, .Query_Features}
128+
}
129+
return nil, nil
130+
131+
case .Query_Info:
132+
return nil, .Mode_Not_Implemented
133+
}
134+
135+
// resizing
136+
if old_memory == nil {
137+
return page_aligned_alloc(size, alignment, granularity, flags)
138+
}
139+
if size == 0 {
140+
return nil, page_free(old_memory, old_size, granularity, flags)
141+
}
142+
return page_aligned_resize(old_memory, old_size, size, alignment, granularity, flags)
143+
}
144+
Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
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

Comments
 (0)