├── .gitignore ├── .gitattributes ├── .gitmodules ├── .github └── workflows │ └── main.yml ├── LICENSE ├── README.md └── gc.zig /.gitignore: -------------------------------------------------------------------------------- 1 | zig-cache -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.zig text eol=lf 2 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "lib/zig-bench"] 2 | path = lib/zig-bench 3 | url = https://github.com/Hejsil/zig-bench.git 4 | branch = master 5 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: push 3 | jobs: 4 | test: 5 | strategy: 6 | matrix: 7 | os: [ubuntu-latest, macos-latest] 8 | runs-on: ${{matrix.os}} 9 | steps: 10 | - uses: actions/checkout@v1 11 | with: 12 | submodules: recursive 13 | - uses: goto-bus-stop/setup-zig@v1.0.0 14 | with: 15 | version: 0.8.0 16 | - run: zig build 17 | lint: 18 | runs-on: ubuntu-latest 19 | steps: 20 | - uses: actions/checkout@v1 21 | - uses: goto-bus-stop/setup-zig@v1.0.0 22 | with: 23 | version: 0.8.0 24 | - run: zig fmt --check . 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # zig-bench 2 | 3 | A simple Garbage Collector in Zig. This GC is pretty bad, and mostly just a proof of concept. 4 | 5 | ## Benchmarks 6 | 7 | ``` 8 | Benchmark Arg Mean(ns) 9 | --------------------------------------------------------------- 10 | DirectAllocator 0 9867 11 | DirectAllocator 1 20578 12 | DirectAllocator 2 37061 13 | DirectAllocator 3 10285 14 | DirectAllocator 4 19847 15 | DirectAllocator 5 35768 16 | DirectAllocator 6 9291 17 | DirectAllocator 7 18095 18 | DirectAllocator 8 35587 19 | Arena_DirectAllocator 0 5796 20 | Arena_DirectAllocator 1 8054 21 | Arena_DirectAllocator 2 10330 22 | Arena_DirectAllocator 3 9932 23 | Arena_DirectAllocator 4 12132 24 | Arena_DirectAllocator 5 14381 25 | Arena_DirectAllocator 6 10039 26 | Arena_DirectAllocator 7 12176 27 | Arena_DirectAllocator 8 14335 28 | GcAllocator_DirectAllocator 0 23010 29 | GcAllocator_DirectAllocator 1 53555 30 | GcAllocator_DirectAllocator 2 96861 31 | GcAllocator_DirectAllocator 3 28409 32 | GcAllocator_DirectAllocator 4 70079 33 | GcAllocator_DirectAllocator 5 127971 34 | GcAllocator_DirectAllocator 6 41809 35 | GcAllocator_DirectAllocator 7 113122 36 | GcAllocator_DirectAllocator 8 212150 37 | FixedBufferAllocator 0 118 38 | FixedBufferAllocator 1 198 39 | FixedBufferAllocator 2 338 40 | FixedBufferAllocator 3 98 41 | FixedBufferAllocator 4 190 42 | FixedBufferAllocator 5 353 43 | FixedBufferAllocator 6 97 44 | FixedBufferAllocator 7 177 45 | FixedBufferAllocator 8 340 46 | Arena_FixedBufferAllocator 0 125 47 | Arena_FixedBufferAllocator 1 220 48 | Arena_FixedBufferAllocator 2 436 49 | Arena_FixedBufferAllocator 3 145 50 | Arena_FixedBufferAllocator 4 232 51 | Arena_FixedBufferAllocator 5 401 52 | Arena_FixedBufferAllocator 6 144 53 | Arena_FixedBufferAllocator 7 248 54 | Arena_FixedBufferAllocator 8 491 55 | GcAllocator_FixedBufferAllocator 0 23160 56 | GcAllocator_FixedBufferAllocator 1 69917 57 | GcAllocator_FixedBufferAllocator 2 198616 58 | GcAllocator_FixedBufferAllocator 3 85539 59 | GcAllocator_FixedBufferAllocator 4 352586 60 | GcAllocator_FixedBufferAllocator 5 1849736 61 | GcAllocator_FixedBufferAllocator 6 269965 62 | GcAllocator_FixedBufferAllocator 7 1691938 63 | GcAllocator_FixedBufferAllocator 8 3105935 64 | OK 65 | All tests passed. 66 | ``` 67 | -------------------------------------------------------------------------------- /gc.zig: -------------------------------------------------------------------------------- 1 | const bench = @import("bench"); 2 | const std = @import("std"); 3 | 4 | const builtin = std.builtin; 5 | const debug = std.debug; 6 | const heap = std.heap; 7 | const mem = std.mem; 8 | const testing = std.testing; 9 | 10 | pub const GcAllocator = struct { 11 | const PointerList = std.ArrayList(Pointer); 12 | 13 | base: mem.Allocator, 14 | start: [*]const u8, 15 | ptrs: PointerList, 16 | 17 | const Flags = packed struct { 18 | checked: bool, 19 | marked: bool, 20 | 21 | const zero = Flags{ 22 | .checked = false, 23 | .marked = false, 24 | }; 25 | }; 26 | 27 | const Pointer = struct { 28 | flags: Flags, 29 | memory: []u8, 30 | }; 31 | 32 | pub inline fn init(child_alloc: *mem.Allocator) GcAllocator { 33 | return GcAllocator{ 34 | .base = mem.Allocator{ 35 | .allocFn = allocFn, 36 | .resizeFn = resizeFn, 37 | }, 38 | .start = @intToPtr([*]const u8, @frameAddress()), 39 | .ptrs = PointerList.init(child_alloc), 40 | }; 41 | } 42 | 43 | pub fn deinit(gc: *GcAllocator) void { 44 | const child_alloc = gc.childAllocator(); 45 | 46 | for (gc.ptrs.items) |ptr| { 47 | child_alloc.free(ptr.memory); 48 | } 49 | 50 | gc.ptrs.deinit(); 51 | gc.* = undefined; 52 | } 53 | 54 | pub fn collect(gc: *GcAllocator) void { 55 | @call(.{ .modifier = .never_inline }, collectNoInline, .{gc}); 56 | } 57 | 58 | pub fn collectFrame(gc: *GcAllocator, frame: []const u8) void { 59 | gc.mark(frame); 60 | gc.sweep(); 61 | } 62 | 63 | pub fn allocator(gc: *GcAllocator) *mem.Allocator { 64 | return &gc.base; 65 | } 66 | 67 | fn collectNoInline(gc: *GcAllocator) void { 68 | const frame = blk: { 69 | const end = @intToPtr([*]const u8, @frameAddress()); 70 | const i_start = @ptrToInt(gc.start); 71 | const i_end = @ptrToInt(end); 72 | if (i_start < i_end) 73 | break :blk gc.start[0 .. i_end - i_start]; 74 | 75 | break :blk end[0 .. i_start - i_end]; 76 | }; 77 | gc.collectFrame(frame); 78 | } 79 | 80 | fn mark(gc: *GcAllocator, frame: []const u8) void { 81 | const ptr_size = @sizeOf(*u8); 82 | for ([_]void{{}} ** ptr_size) |_, i| { 83 | if (frame.len <= i) 84 | break; 85 | 86 | const frame2 = frame[i..]; 87 | const len = (frame2.len / ptr_size) * ptr_size; 88 | for (std.mem.bytesAsSlice([*]u8, frame2[0..len])) |frame_ptr| { 89 | const ptr = gc.findPtr(frame_ptr) orelse continue; 90 | if (ptr.flags.checked) 91 | continue; 92 | 93 | ptr.flags.marked = true; 94 | ptr.flags.checked = true; 95 | gc.mark(ptr.memory); 96 | } 97 | } 98 | } 99 | 100 | fn sweep(gc: *GcAllocator) void { 101 | const child_alloc = gc.childAllocator(); 102 | const ptrs = gc.ptrs.items; 103 | var i: usize = 0; 104 | while (i < gc.ptrs.items.len) { 105 | const ptr = &ptrs[i]; 106 | if (ptr.flags.marked) { 107 | ptr.flags = Flags.zero; 108 | i += 1; 109 | continue; 110 | } 111 | 112 | gc.freePtr(ptr); 113 | } 114 | } 115 | 116 | fn findPtr(gc: *GcAllocator, to_find_ptr: anytype) ?*Pointer { 117 | comptime debug.assert(@typeInfo(@TypeOf(to_find_ptr)) == builtin.TypeId.Pointer); 118 | 119 | for (gc.ptrs.items) |*ptr| { 120 | const ptr_start = @ptrToInt(ptr.memory.ptr); 121 | const ptr_end = ptr_start + ptr.memory.len; 122 | if (ptr_start <= @ptrToInt(to_find_ptr) and @ptrToInt(to_find_ptr) < ptr_end) 123 | return ptr; 124 | } 125 | 126 | return null; 127 | } 128 | 129 | fn freePtr(gc: *GcAllocator, ptr: *Pointer) void { 130 | const child_alloc = gc.childAllocator(); 131 | child_alloc.free(ptr.memory); 132 | 133 | // Swap the just freed pointer with the last pointer in the list. 134 | ptr.* = undefined; 135 | ptr.* = gc.ptrs.popOrNull() orelse undefined; 136 | } 137 | 138 | fn childAllocator(gc: *GcAllocator) *mem.Allocator { 139 | return gc.ptrs.allocator; 140 | } 141 | 142 | fn free(base: *mem.Allocator, bytes: []u8) void { 143 | const gc = @fieldParentPtr(GcAllocator, "base", base); 144 | const child_alloc = gc.childAllocator(); 145 | const ptr = gc.findPtr(bytes.ptr) orelse @panic("Freeing memory not allocated by garbage collector!"); 146 | gc.freePtr(ptr); 147 | } 148 | 149 | fn allocFn( 150 | base: *mem.Allocator, 151 | len: usize, 152 | ptr_align: u29, 153 | len_align: u29, 154 | ret_addr: usize, 155 | ) ![]u8 { 156 | const gc = @fieldParentPtr(GcAllocator, "base", base); 157 | const child_alloc = gc.childAllocator(); 158 | const memory = try child_alloc.allocFn(child_alloc, len, ptr_align, len_align, ret_addr); 159 | try gc.ptrs.append(Pointer{ 160 | .flags = Flags.zero, 161 | .memory = memory, 162 | }); 163 | 164 | return memory; 165 | } 166 | 167 | fn resizeFn( 168 | base: *mem.Allocator, 169 | buf: []u8, 170 | buf_align: u29, 171 | new_len: usize, 172 | len_align: u29, 173 | ret_addr: usize, 174 | ) !usize { 175 | if (new_len == 0) { 176 | free(base, buf); 177 | return 0; 178 | } 179 | if (new_len > buf.len) 180 | return error.OutOfMemory; 181 | return new_len; 182 | } 183 | }; 184 | 185 | const Leaker = struct { 186 | l: *Leaker, 187 | }; 188 | 189 | var test_buf: [1024 * 1024]u8 = undefined; 190 | 191 | test "gc.collect: No leaks" { 192 | var fba = heap.FixedBufferAllocator.init(test_buf[0..]); 193 | var gc = GcAllocator.init(&fba.allocator); 194 | defer gc.deinit(); 195 | 196 | const allocator = gc.allocator(); 197 | 198 | var a = try allocator.create(Leaker); 199 | a.* = Leaker{ .l = try allocator.create(Leaker) }; 200 | a.l.l = a; 201 | gc.collect(); 202 | 203 | try testing.expect(gc.findPtr(a) != null); 204 | try testing.expect(gc.findPtr(a.l) != null); 205 | try testing.expectEqual(@as(usize, 2), gc.ptrs.items.len); 206 | } 207 | 208 | fn leak(allocator: *mem.Allocator) !void { 209 | var a = try allocator.create(Leaker); 210 | a.* = Leaker{ .l = try allocator.create(Leaker) }; 211 | a.l.l = a; 212 | } 213 | 214 | test "gc.collect: Leaks" { 215 | var fba = heap.FixedBufferAllocator.init(test_buf[0..]); 216 | var gc = GcAllocator.init(&fba.allocator); 217 | defer gc.deinit(); 218 | 219 | const allocator = gc.allocator(); 220 | 221 | var a = try allocator.create(Leaker); 222 | a.* = Leaker{ .l = try allocator.create(Leaker) }; 223 | a.l.l = a; 224 | try @call(.{ .modifier = .never_inline }, leak, .{allocator}); 225 | gc.collect(); 226 | 227 | try testing.expect(gc.findPtr(a) != null); 228 | try testing.expect(gc.findPtr(a.l) != null); 229 | try testing.expectEqual(@as(usize, 2), gc.ptrs.items.len); 230 | } 231 | 232 | test "gc.free" { 233 | var fba = heap.FixedBufferAllocator.init(test_buf[0..]); 234 | var gc = GcAllocator.init(&fba.allocator); 235 | defer gc.deinit(); 236 | 237 | const allocator = gc.allocator(); 238 | 239 | var a = try allocator.create(Leaker); 240 | var b = try allocator.create(Leaker); 241 | allocator.destroy(b); 242 | 243 | try testing.expect(gc.findPtr(a) != null); 244 | try testing.expectEqual(@as(usize, 1), gc.ptrs.items.len); 245 | } 246 | 247 | test "gc.benchmark" { 248 | try bench.benchmark(struct { 249 | const Arg = struct { 250 | num: usize, 251 | size: usize, 252 | 253 | fn benchAllocator(a: Arg, allocator: *mem.Allocator, comptime free: bool) !void { 254 | var i: usize = 0; 255 | while (i < a.num) : (i += 1) { 256 | const bytes = try allocator.alloc(u8, a.size); 257 | defer if (free) allocator.free(bytes); 258 | } 259 | } 260 | }; 261 | 262 | pub const args = [_]Arg{ 263 | Arg{ .num = 10 * 1, .size = 1024 * 1 }, 264 | Arg{ .num = 10 * 2, .size = 1024 * 1 }, 265 | Arg{ .num = 10 * 4, .size = 1024 * 1 }, 266 | Arg{ .num = 10 * 1, .size = 1024 * 2 }, 267 | Arg{ .num = 10 * 2, .size = 1024 * 2 }, 268 | Arg{ .num = 10 * 4, .size = 1024 * 2 }, 269 | Arg{ .num = 10 * 1, .size = 1024 * 4 }, 270 | Arg{ .num = 10 * 2, .size = 1024 * 4 }, 271 | Arg{ .num = 10 * 4, .size = 1024 * 4 }, 272 | }; 273 | 274 | pub const iterations = 10000; 275 | 276 | pub fn FixedBufferAllocator(a: Arg) void { 277 | var fba = heap.FixedBufferAllocator.init(test_buf[0..]); 278 | a.benchAllocator(&fba.allocator, false) catch unreachable; 279 | } 280 | 281 | pub fn Arena_FixedBufferAllocator(a: Arg) void { 282 | var fba = heap.FixedBufferAllocator.init(test_buf[0..]); 283 | var arena = heap.ArenaAllocator.init(&fba.allocator); 284 | defer arena.deinit(); 285 | 286 | a.benchAllocator(&arena.allocator, false) catch unreachable; 287 | } 288 | 289 | pub fn GcAllocator_FixedBufferAllocator(a: Arg) void { 290 | var fba = heap.FixedBufferAllocator.init(test_buf[0..]); 291 | var gc = GcAllocator.init(&fba.allocator); 292 | defer gc.deinit(); 293 | 294 | a.benchAllocator(gc.allocator(), false) catch unreachable; 295 | gc.collect(); 296 | } 297 | 298 | pub fn PageAllocator(a: Arg) void { 299 | const pa = heap.page_allocator; 300 | 301 | a.benchAllocator(pa, true) catch unreachable; 302 | } 303 | 304 | pub fn Arena_PageAllocator(a: Arg) void { 305 | const pa = heap.page_allocator; 306 | 307 | var arena = heap.ArenaAllocator.init(pa); 308 | defer arena.deinit(); 309 | 310 | a.benchAllocator(&arena.allocator, false) catch unreachable; 311 | } 312 | 313 | pub fn GcAllocator_PageAllocator(a: Arg) void { 314 | const pa = heap.page_allocator; 315 | 316 | var gc = GcAllocator.init(pa); 317 | defer gc.deinit(); 318 | 319 | a.benchAllocator(gc.allocator(), false) catch unreachable; 320 | gc.collect(); 321 | } 322 | }); 323 | } 324 | --------------------------------------------------------------------------------