authorgravatar for sahnvour@pm.meSahnvour <sahnvour@pm.me> 2019-05-08 18:02:37+02:00
committergravatar for sahnvour@pm.meSahnvour <sahnvour@pm.me> 2019-05-11 16:41:13+02:00
loga2d5b0fabe13d28075963e06b4bc6040888542af
tree242c951e7af032816998d5f4ea23786d6a4cd76c
parentd665948dcf74cb4fff7e815dbcdabe363a337c76

Implement Windows' DirectAllocator on top of VirtualAlloc and VirtualFree.


3 files changed, 177 insertions(+), 69 deletions(-)

std/heap.zig+143-69
...@@ -34,9 +34,6 @@ fn cShrink(self: *Allocator, old_mem: []u8, old_align: u29, new_size: usize, new...@@ -34,9 +34,6 @@ fn cShrink(self: *Allocator, old_mem: []u8, old_align: u29, new_size: usize, new
34/// Thread-safe and lock-free.34/// Thread-safe and lock-free.
35pub const DirectAllocator = struct {35pub const DirectAllocator = struct {
36 allocator: Allocator,36 allocator: Allocator,
37 heap_handle: ?HeapHandle,
38
39 const HeapHandle = if (builtin.os == Os.windows) os.windows.HANDLE else void;
4037
41 pub fn init() DirectAllocator {38 pub fn init() DirectAllocator {
42 return DirectAllocator{39 return DirectAllocator{
...@@ -44,18 +41,10 @@ pub const DirectAllocator = struct {...@@ -44,18 +41,10 @@ pub const DirectAllocator = struct {
44 .reallocFn = realloc,41 .reallocFn = realloc,
45 .shrinkFn = shrink,42 .shrinkFn = shrink,
46 },43 },
47 .heap_handle = if (builtin.os == Os.windows) null else {},
48 };44 };
49 }45 }
5046
51 pub fn deinit(self: *DirectAllocator) void {47 pub fn deinit(self: *DirectAllocator) void {}
52 switch (builtin.os) {
53 Os.windows => if (self.heap_handle) |heap_handle| {
54 _ = os.windows.HeapDestroy(heap_handle);
55 },
56 else => {},
57 }
58 }
5948
60 fn alloc(allocator: *Allocator, n: usize, alignment: u29) error{OutOfMemory}![]u8 {49 fn alloc(allocator: *Allocator, n: usize, alignment: u29) error{OutOfMemory}![]u8 {
61 const self = @fieldParentPtr(DirectAllocator, "allocator", allocator);50 const self = @fieldParentPtr(DirectAllocator, "allocator", allocator);
...@@ -89,21 +78,57 @@ pub const DirectAllocator = struct {...@@ -89,21 +78,57 @@ pub const DirectAllocator = struct {
8978
90 return @intToPtr([*]u8, aligned_addr)[0..n];79 return @intToPtr([*]u8, aligned_addr)[0..n];
91 },80 },
92 Os.windows => {81 .windows => {
93 const amt = n + alignment + @sizeOf(usize);82 const w = os.windows;
94 const optional_heap_handle = @atomicLoad(?HeapHandle, &self.heap_handle, builtin.AtomicOrder.SeqCst);83
95 const heap_handle = optional_heap_handle orelse blk: {84 // Although officially it's at least aligned to page boundary,
96 const hh = os.windows.HeapCreate(0, amt, 0) orelse return error.OutOfMemory;85 // Windows is known to reserve pages on a 64K boundary. It's
97 const other_hh = @cmpxchgStrong(?HeapHandle, &self.heap_handle, null, hh, builtin.AtomicOrder.SeqCst, builtin.AtomicOrder.SeqCst) orelse break :blk hh;86 // even more likely that the requested alignment is <= 64K than
98 _ = os.windows.HeapDestroy(hh);87 // 4K, so we're just allocating blindly and hoping for the best.
99 break :blk other_hh.?; // can't be null because of the cmpxchg88 // see https://devblogs.microsoft.com/oldnewthing/?p=42223
100 };89 const addr = w.VirtualAlloc(
101 const ptr = os.windows.HeapAlloc(heap_handle, 0, amt) orelse return error.OutOfMemory;90 null,
102 const root_addr = @ptrToInt(ptr);91 n,
103 const adjusted_addr = mem.alignForward(root_addr, alignment);92 w.MEM_COMMIT | w.MEM_RESERVE,
104 const record_addr = adjusted_addr + n;93 w.PAGE_READWRITE,
105 @intToPtr(*align(1) usize, record_addr).* = root_addr;94 ) orelse return error.OutOfMemory;
106 return @intToPtr([*]u8, adjusted_addr)[0..n];95
96 // If the allocation is sufficiently aligned, use it.
97 if (@ptrToInt(addr) & (alignment - 1) == 0) {
98 return @ptrCast([*]u8, addr)[0..n];
99 }
100
101 // If it wasn't, actually do an explicitely aligned allocation.
102 if (w.VirtualFree(addr, 0, w.MEM_RELEASE) == 0) unreachable;
103 const alloc_size = n + alignment;
104
105 const final_addr = while (true) {
106 // Reserve a range of memory large enough to find a sufficiently
107 // aligned address.
108 const reserved_addr = w.VirtualAlloc(
109 null,
110 alloc_size,
111 w.MEM_RESERVE,
112 w.PAGE_NOACCESS,
113 ) orelse return error.OutOfMemory;
114 const aligned_addr = mem.alignForward(@ptrToInt(reserved_addr), alignment);
115
116 // Release the reserved pages (not actually used).
117 if (w.VirtualFree(reserved_addr, 0, w.MEM_RELEASE) == 0) unreachable;
118
119 // At this point, it is possible that another thread has
120 // obtained some memory space that will cause the next
121 // VirtualAlloc call to fail. To handle this, we will retry
122 // until it succeeds.
123 if (w.VirtualAlloc(
124 @intToPtr(*c_void, aligned_addr),
125 n,
126 w.MEM_COMMIT | w.MEM_RESERVE,
127 w.PAGE_READWRITE,
128 )) |ptr| break ptr;
129 } else unreachable; // TODO else unreachable should not be necessary
130
131 return @ptrCast([*]u8, final_addr)[0..n];
107 },132 },
108 else => @compileError("Unsupported OS"),133 else => @compileError("Unsupported OS"),
109 }134 }
...@@ -121,13 +146,31 @@ pub const DirectAllocator = struct {...@@ -121,13 +146,31 @@ pub const DirectAllocator = struct {
121 }146 }
122 return old_mem[0..new_size];147 return old_mem[0..new_size];
123 },148 },
124 Os.windows => return realloc(allocator, old_mem, old_align, new_size, new_align) catch {149 .windows => {
125 const old_adjusted_addr = @ptrToInt(old_mem.ptr);150 const w = os.windows;
126 const old_record_addr = old_adjusted_addr + old_mem.len;151 if (new_size == 0) {
127 const root_addr = @intToPtr(*align(1) usize, old_record_addr).*;152 // From the docs:
128 const old_ptr = @intToPtr(*c_void, root_addr);153 // "If the dwFreeType parameter is MEM_RELEASE, this parameter
129 const new_record_addr = old_record_addr - new_size + old_mem.len;154 // must be 0 (zero). The function frees the entire region that
130 @intToPtr(*align(1) usize, new_record_addr).* = root_addr;155 // is reserved in the initial allocation call to VirtualAlloc."
156 // So we can only use MEM_RELEASE when actually releasing the
157 // whole allocation.
158 if (w.VirtualFree(old_mem.ptr, 0, w.MEM_RELEASE) == 0) unreachable;
159 } else {
160 const base_addr = @ptrToInt(old_mem.ptr);
161 const old_addr_end = base_addr + old_mem.len;
162 const new_addr_end = base_addr + new_size;
163 const new_addr_end_rounded = mem.alignForward(new_addr_end, os.page_size);
164 if (old_addr_end > new_addr_end_rounded) {
165 // For shrinking that is not releasing, we will only
166 // decommit the pages not needed anymore.
167 if (w.VirtualFree(
168 @intToPtr(*c_void, new_addr_end_rounded),
169 old_addr_end - new_addr_end_rounded,
170 w.MEM_DECOMMIT,
171 ) == 0) unreachable;
172 }
173 }
131 return old_mem[0..new_size];174 return old_mem[0..new_size];
132 },175 },
133 else => @compileError("Unsupported OS"),176 else => @compileError("Unsupported OS"),
...@@ -147,43 +190,59 @@ pub const DirectAllocator = struct {...@@ -147,43 +190,59 @@ pub const DirectAllocator = struct {
147 }190 }
148 return result;191 return result;
149 },192 },
150 Os.windows => {193 .windows => {
151 if (old_mem.len == 0) return alloc(allocator, new_size, new_align);194 if (old_mem.len == 0) {
195 return alloc(allocator, new_size, new_align);
196 }
152197
153 const self = @fieldParentPtr(DirectAllocator, "allocator", allocator);198 if (new_size <= old_mem.len and new_align <= old_align) {
154 const old_adjusted_addr = @ptrToInt(old_mem.ptr);199 return shrink(allocator, old_mem, old_align, new_size, new_align);
155 const old_record_addr = old_adjusted_addr + old_mem.len;200 }
156 const root_addr = @intToPtr(*align(1) usize, old_record_addr).*;
157 const old_ptr = @intToPtr(*c_void, root_addr);
158201
159 if (new_size == 0) {202 const w = os.windows;
160 if (os.windows.HeapFree(self.heap_handle.?, 0, old_ptr) == 0) unreachable;203 const base_addr = @ptrToInt(old_mem.ptr);
161 return old_mem[0..0];204
205 if (new_align > old_align and base_addr & (new_align - 1) != 0) {
206 // Current allocation doesn't satisfy the new alignment.
207 // For now we'll do a new one no matter what, but maybe
208 // there is something smarter to do instead.
209 const result = try alloc(allocator, new_size, new_align);
210 assert(old_mem.len != 0);
211 @memcpy(result.ptr, old_mem.ptr, std.math.min(old_mem.len, result.len));
212 if (w.VirtualFree(old_mem.ptr, 0, w.MEM_RELEASE) == 0) unreachable;
213
214 return result;
162 }215 }
163216
164 const amt = new_size + new_align + @sizeOf(usize);217 const old_addr_end = base_addr + old_mem.len;
165 const new_ptr = os.windows.HeapReAlloc(218 const old_addr_end_rounded = mem.alignForward(old_addr_end, os.page_size);
166 self.heap_handle.?,219 const new_addr_end = base_addr + new_size;
167 0,220 const new_addr_end_rounded = mem.alignForward(new_addr_end, os.page_size);
168 old_ptr,221 if (new_addr_end_rounded == old_addr_end_rounded) {
169 amt,222 // The reallocation fits in the already allocated pages.
170 ) orelse return error.OutOfMemory;223 return @ptrCast([*]u8, old_mem.ptr)[0..new_size];
171 const offset = old_adjusted_addr - root_addr;
172 const new_root_addr = @ptrToInt(new_ptr);
173 var new_adjusted_addr = new_root_addr + offset;
174 const offset_is_valid = new_adjusted_addr + new_size + @sizeOf(usize) <= new_root_addr + amt;
175 const offset_is_aligned = new_adjusted_addr % new_align == 0;
176 if (!offset_is_valid or !offset_is_aligned) {
177 // If HeapReAlloc didn't happen to move the memory to the new alignment,
178 // or the memory starting at the old offset would be outside of the new allocation,
179 // then we need to copy the memory to a valid aligned address and use that
180 const new_aligned_addr = mem.alignForward(new_root_addr, new_align);
181 @memcpy(@intToPtr([*]u8, new_aligned_addr), @intToPtr([*]u8, new_adjusted_addr), std.math.min(old_mem.len, new_size));
182 new_adjusted_addr = new_aligned_addr;
183 }224 }
184 const new_record_addr = new_adjusted_addr + new_size;225 assert(new_addr_end_rounded > old_addr_end_rounded);
185 @intToPtr(*align(1) usize, new_record_addr).* = new_root_addr;226
186 return @intToPtr([*]u8, new_adjusted_addr)[0..new_size];227 // We need to commit new pages.
228 const additional_size = new_addr_end - old_addr_end_rounded;
229 const realloc_addr = w.VirtualAlloc(
230 @intToPtr(*c_void, old_addr_end_rounded),
231 additional_size,
232 w.MEM_COMMIT | w.MEM_RESERVE,
233 w.PAGE_READWRITE,
234 ) orelse {
235 // Committing new pages at the end of the existing allocation
236 // failed, we need to try a new one.
237 const new_alloc_mem = try alloc(allocator, new_size, new_align);
238 @memcpy(new_alloc_mem.ptr, old_mem.ptr, old_mem.len);
239 if (w.VirtualFree(old_mem.ptr, 0, w.MEM_RELEASE) == 0) unreachable;
240
241 return new_alloc_mem;
242 };
243
244 assert(@ptrToInt(realloc_addr) == old_addr_end_rounded);
245 return @ptrCast([*]u8, old_mem.ptr)[0..new_size];
187 },246 },
188 else => @compileError("Unsupported OS"),247 else => @compileError("Unsupported OS"),
189 }248 }
...@@ -686,6 +745,17 @@ test "DirectAllocator" {...@@ -686,6 +745,17 @@ test "DirectAllocator" {
686 try testAllocatorAligned(allocator, 16);745 try testAllocatorAligned(allocator, 16);
687 try testAllocatorLargeAlignment(allocator);746 try testAllocatorLargeAlignment(allocator);
688 try testAllocatorAlignedShrink(allocator);747 try testAllocatorAlignedShrink(allocator);
748
749 if (builtin.os == .windows) {
750 // Trying really large alignment. As mentionned in the implementation,
751 // VirtualAlloc returns 64K aligned addresses. We want to make sure
752 // DirectAllocator works beyond that, as it's not tested by
753 // `testAllocatorLargeAlignment`.
754 const slice = try allocator.alignedAlloc(u8, 1 << 20, 128);
755 slice[0] = 0x12;
756 slice[127] = 0x34;
757 allocator.free(slice);
758 }
689}759}
690760
691test "HeapAllocator" {761test "HeapAllocator" {
...@@ -714,7 +784,7 @@ test "ArenaAllocator" {...@@ -714,7 +784,7 @@ test "ArenaAllocator" {
714 try testAllocatorAlignedShrink(&arena_allocator.allocator);784 try testAllocatorAlignedShrink(&arena_allocator.allocator);
715}785}
716786
717var test_fixed_buffer_allocator_memory: [40000 * @sizeOf(u64)]u8 = undefined;787var test_fixed_buffer_allocator_memory: [80000 * @sizeOf(u64)]u8 = undefined;
718test "FixedBufferAllocator" {788test "FixedBufferAllocator" {
719 var fixed_buffer_allocator = FixedBufferAllocator.init(test_fixed_buffer_allocator_memory[0..]);789 var fixed_buffer_allocator = FixedBufferAllocator.init(test_fixed_buffer_allocator_memory[0..]);
720790
...@@ -852,7 +922,11 @@ fn testAllocatorAlignedShrink(allocator: *mem.Allocator) mem.Allocator.Error!voi...@@ -852,7 +922,11 @@ fn testAllocatorAlignedShrink(allocator: *mem.Allocator) mem.Allocator.Error!voi
852 defer allocator.free(slice);922 defer allocator.free(slice);
853923
854 var stuff_to_free = std.ArrayList([]align(16) u8).init(debug_allocator);924 var stuff_to_free = std.ArrayList([]align(16) u8).init(debug_allocator);
855 while (@ptrToInt(slice.ptr) == mem.alignForward(@ptrToInt(slice.ptr), os.page_size * 2)) {925 // On Windows, VirtualAlloc returns addresses aligned to a 64K boundary,
926 // which is 16 pages, hence the 32. This test may require to increase
927 // the size of the allocations feeding the `allocator` parameter if they
928 // fail, because of this high over-alignment we want to have.
929 while (@ptrToInt(slice.ptr) == mem.alignForward(@ptrToInt(slice.ptr), os.page_size * 32)) {
856 try stuff_to_free.append(slice);930 try stuff_to_free.append(slice);
857 slice = try allocator.alignedAlloc(u8, 16, alloc_size);931 slice = try allocator.alignedAlloc(u8, 16, alloc_size);
858 }932 }
...@@ -863,7 +937,7 @@ fn testAllocatorAlignedShrink(allocator: *mem.Allocator) mem.Allocator.Error!voi...@@ -863,7 +937,7 @@ fn testAllocatorAlignedShrink(allocator: *mem.Allocator) mem.Allocator.Error!voi
863 slice[60] = 0x34;937 slice[60] = 0x34;
864938
865 // realloc to a smaller size but with a larger alignment939 // realloc to a smaller size but with a larger alignment
866 slice = try allocator.alignedRealloc(slice, os.page_size * 2, alloc_size / 2);940 slice = try allocator.alignedRealloc(slice, os.page_size * 32, alloc_size / 2);
867 testing.expect(slice[0] == 0x12);941 testing.expect(slice[0] == 0x12);
868 testing.expect(slice[60] == 0x34);942 testing.expect(slice[60] == 0x34);
869}943}
std/os/windows.zig+31
...@@ -239,6 +239,37 @@ pub const HEAP_CREATE_ENABLE_EXECUTE = 0x00040000;...@@ -239,6 +239,37 @@ pub const HEAP_CREATE_ENABLE_EXECUTE = 0x00040000;
239pub const HEAP_GENERATE_EXCEPTIONS = 0x00000004;239pub const HEAP_GENERATE_EXCEPTIONS = 0x00000004;
240pub const HEAP_NO_SERIALIZE = 0x00000001;240pub const HEAP_NO_SERIALIZE = 0x00000001;
241241
242// AllocationType values
243pub const MEM_COMMIT = 0x1000;
244pub const MEM_RESERVE = 0x2000;
245pub const MEM_RESET = 0x80000;
246pub const MEM_RESET_UNDO = 0x1000000;
247pub const MEM_LARGE_PAGES = 0x20000000;
248pub const MEM_PHYSICAL = 0x400000;
249pub const MEM_TOP_DOWN = 0x100000;
250pub const MEM_WRITE_WATCH = 0x200000;
251
252// Protect values
253pub const PAGE_EXECUTE = 0x10;
254pub const PAGE_EXECUTE_READ = 0x20;
255pub const PAGE_EXECUTE_READWRITE = 0x40;
256pub const PAGE_EXECUTE_WRITECOPY = 0x80;
257pub const PAGE_NOACCESS = 0x01;
258pub const PAGE_READONLY = 0x02;
259pub const PAGE_READWRITE = 0x04;
260pub const PAGE_WRITECOPY = 0x08;
261pub const PAGE_TARGETS_INVALID = 0x40000000;
262pub const PAGE_TARGETS_NO_UPDATE = 0x40000000; // Same as PAGE_TARGETS_INVALID
263pub const PAGE_GUARD = 0x100;
264pub const PAGE_NOCACHE = 0x200;
265pub const PAGE_WRITECOMBINE = 0x400;
266
267// FreeType values
268pub const MEM_COALESCE_PLACEHOLDERS = 0x1;
269pub const MEM_RESERVE_PLACEHOLDERS = 0x2;
270pub const MEM_DECOMMIT = 0x4000;
271pub const MEM_RELEASE = 0x8000;
272
242pub const PTHREAD_START_ROUTINE = extern fn (LPVOID) DWORD;273pub const PTHREAD_START_ROUTINE = extern fn (LPVOID) DWORD;
243pub const LPTHREAD_START_ROUTINE = PTHREAD_START_ROUTINE;274pub const LPTHREAD_START_ROUTINE = PTHREAD_START_ROUTINE;
244275
std/os/windows/kernel32.zig+3
...@@ -116,6 +116,9 @@ pub extern "kernel32" stdcallcc fn HeapFree(hHeap: HANDLE, dwFlags: DWORD, lpMem...@@ -116,6 +116,9 @@ pub extern "kernel32" stdcallcc fn HeapFree(hHeap: HANDLE, dwFlags: DWORD, lpMem
116116
117pub extern "kernel32" stdcallcc fn HeapValidate(hHeap: HANDLE, dwFlags: DWORD, lpMem: ?*const c_void) BOOL;117pub extern "kernel32" stdcallcc fn HeapValidate(hHeap: HANDLE, dwFlags: DWORD, lpMem: ?*const c_void) BOOL;
118118
119pub extern "kernel32" stdcallcc fn VirtualAlloc(lpAddress: ?LPVOID, dwSize: SIZE_T, flAllocationType: DWORD, flProtect: DWORD) ?LPVOID;
120pub extern "kernel32" stdcallcc fn VirtualFree(lpAddress: ?LPVOID, dwSize: SIZE_T, dwFreeType: DWORD) BOOL;
121
119pub extern "kernel32" stdcallcc fn MoveFileExW(122pub extern "kernel32" stdcallcc fn MoveFileExW(
120 lpExistingFileName: [*]const u16,123 lpExistingFileName: [*]const u16,
121 lpNewFileName: [*]const u16,124 lpNewFileName: [*]const u16,