| ... | ... | @@ -78,7 +78,7 @@ fn countListCapacity(first_node: ?*Node) usize { |
| 78 | 78 | while (it) |node| : (it = node.next) { |
| 79 | 79 | // Compute the actually allocated size excluding the |
| 80 | 80 | // linked list node. |
| 81 | | capacity += node.size - @sizeOf(Node); |
| 81 | capacity += node.size.toInt() - @sizeOf(Node); |
| 82 | 82 | } |
| 83 | 83 | return capacity; |
| 84 | 84 | } |
| ... | ... | @@ -164,7 +164,10 @@ pub fn reset(arena: *ArenaAllocator, mode: ResetMode) bool { |
| 164 | 164 | }; |
| 165 | 165 | const allocated_slice = node.allocatedSliceUnsafe(); |
| 166 | 166 | |
| 167 | | if (new_capacity == 0) { |
| 167 | // Align backwards to always stay below limit. |
| 168 | const new_size = mem.alignBackward(usize, @sizeOf(Node) + new_capacity, 2); |
| 169 | |
| 170 | if (new_size == @sizeOf(Node)) { |
| 168 | 171 | arena.child_allocator.rawFree(allocated_slice, .of(Node), @returnAddress()); |
| 169 | 172 | first_node_ptr.* = null; |
| 170 | 173 | continue; |
| ... | ... | @@ -173,19 +176,17 @@ pub fn reset(arena: *ArenaAllocator, mode: ResetMode) bool { |
| 173 | 176 | node.end_index = 0; |
| 174 | 177 | first_node_ptr.* = node; |
| 175 | 178 | |
| 176 | | const adjusted_capacity: usize = mem.alignForward(usize, new_capacity, 2); |
| 177 | | |
| 178 | | if (allocated_slice.len - @sizeOf(Node) == adjusted_capacity) { |
| 179 | if (allocated_slice.len == new_size) { |
| 179 | 180 | // perfect, no need to invoke the child_allocator |
| 180 | 181 | continue; |
| 181 | 182 | } |
| 182 | 183 | |
| 183 | | if (arena.child_allocator.rawResize(allocated_slice, .of(Node), adjusted_capacity, @returnAddress())) { |
| 184 | if (arena.child_allocator.rawResize(allocated_slice, .of(Node), new_size, @returnAddress())) { |
| 184 | 185 | // successful resize |
| 185 | | node.size = adjusted_capacity; |
| 186 | node.size = .fromInt(new_size); |
| 186 | 187 | } else { |
| 187 | 188 | // manual realloc |
| 188 | | const new_ptr = arena.child_allocator.rawAlloc(adjusted_capacity, .of(Node), @returnAddress()) orelse { |
| 189 | const new_ptr = arena.child_allocator.rawAlloc(new_size, .of(Node), @returnAddress()) orelse { |
| 189 | 190 | // we failed to preheat the arena properly, signal this to the user. |
| 190 | 191 | ok = false; |
| 191 | 192 | continue; |
| ... | ... | @@ -193,7 +194,7 @@ pub fn reset(arena: *ArenaAllocator, mode: ResetMode) bool { |
| 193 | 194 | arena.child_allocator.rawFree(allocated_slice, .of(Node), @returnAddress()); |
| 194 | 195 | const new_first_node: *Node = @ptrCast(@alignCast(new_ptr)); |
| 195 | 196 | new_first_node.* = .{ |
| 196 | | .size = adjusted_capacity, |
| 197 | .size = .fromInt(new_size), |
| 197 | 198 | .end_index = 0, |
| 198 | 199 | .next = null, |
| 199 | 200 | }; |
| ... | ... | @@ -213,51 +214,60 @@ pub fn reset(arena: *ArenaAllocator, mode: ResetMode) bool { |
| 213 | 214 | const Node = struct { |
| 214 | 215 | /// Only meant to be accessed indirectly via the methods supplied by this type, |
| 215 | 216 | /// except if the node is owned by the thread accessing it. |
| 216 | | /// Must always be an even number to accomodate `resize_bit`. |
| 217 | | size: usize, |
| 218 | | /// Concurrent accesses to `end_index` can be monotonic since it is only ever |
| 219 | | /// incremented in `alloc` and `resize` after being compared to `size`. |
| 217 | /// Must always be an even number to accomodate `resize` bit. |
| 218 | size: Size, |
| 219 | /// Concurrent accesses to `end_index` can be monotonic as long as its value |
| 220 | /// is compared to a version of `size` before using it to access memory. |
| 220 | 221 | /// Since `size` can only grow and never shrink, memory access depending on |
| 221 | | /// `end_index` can never be OOB. |
| 222 | /// any `end_index` <= any `size` can never be OOB. |
| 222 | 223 | end_index: usize, |
| 223 | 224 | /// This field should only be accessed if the node is owned by the thread |
| 224 | 225 | /// accessing it. |
| 225 | 226 | next: ?*Node, |
| 226 | 227 | |
| 227 | | const resize_bit: usize = 1; |
| 228 | const Size = packed struct(usize) { |
| 229 | resizing: bool, |
| 230 | _: @Int(.unsigned, @bitSizeOf(usize) - 1) = 0, |
| 228 | 231 | |
| 229 | | fn loadEndIndex(node: *Node) usize { |
| 230 | | return @atomicLoad(usize, &node.end_index, .monotonic); |
| 231 | | } |
| 232 | fn fromInt(int: usize) Size { |
| 233 | assert(int >= @sizeOf(Node)); |
| 234 | const size: Size = @bitCast(int); |
| 235 | assert(!size.resizing); |
| 236 | return size; |
| 237 | } |
| 232 | 238 | |
| 233 | | /// Returns `null` on success and previous value on failure. |
| 234 | | fn trySetEndIndex(node: *Node, from: usize, to: usize) ?usize { |
| 235 | | assert(from != to); // check this before attempting to set `end_index`! |
| 236 | | return @cmpxchgWeak(usize, &node.end_index, from, to, .monotonic, .monotonic); |
| 237 | | } |
| 239 | fn toInt(size: Size) usize { |
| 240 | var int = size; |
| 241 | int.resizing = false; |
| 242 | return @bitCast(int); |
| 243 | } |
| 244 | |
| 245 | comptime { |
| 246 | assert(Size{ .resizing = true } == @as(Size, @bitCast(@as(usize, 1)))); |
| 247 | } |
| 248 | }; |
| 238 | 249 | |
| 239 | 250 | fn loadBuf(node: *Node) []u8 { |
| 240 | 251 | // monotonic is fine since `size` can only ever grow, so the buffer returned |
| 241 | 252 | // by this function is always valid memory. |
| 242 | | const size = @atomicLoad(usize, &node.size, .monotonic); |
| 243 | | return @as([*]u8, @ptrCast(node))[0 .. size & ~resize_bit][@sizeOf(Node)..]; |
| 253 | const size = @atomicLoad(Size, &node.size, .monotonic); |
| 254 | return @as([*]u8, @ptrCast(node))[0..size.toInt()][@sizeOf(Node)..]; |
| 244 | 255 | } |
| 245 | 256 | |
| 246 | 257 | /// Returns allocated slice or `null` if node is already (being) resized. |
| 247 | 258 | fn beginResize(node: *Node) ?[]u8 { |
| 248 | | const size = @atomicRmw(usize, &node.size, .Or, resize_bit, .acquire); // syncs with release in `endResize` |
| 249 | | if (size & resize_bit != 0) return null; |
| 250 | | return @as([*]u8, @ptrCast(node))[0..size]; |
| 259 | const size = @atomicRmw(Size, &node.size, .Or, .{ .resizing = true }, .acquire); // syncs with release in `endResize` |
| 260 | if (size.resizing) return null; |
| 261 | return @as([*]u8, @ptrCast(node))[0..size.toInt()]; |
| 251 | 262 | } |
| 252 | 263 | |
| 253 | 264 | fn endResize(node: *Node, size: usize) void { |
| 254 | | assert(size & resize_bit == 0); |
| 255 | | return @atomicStore(usize, &node.size, size, .release); // syncs with acquire in `beginResize` |
| 265 | return @atomicStore(Size, &node.size, .fromInt(size), .release); // syncs with acquire in `beginResize` |
| 256 | 266 | } |
| 257 | 267 | |
| 258 | 268 | /// Not threadsafe. |
| 259 | 269 | fn allocatedSliceUnsafe(node: *Node) []u8 { |
| 260 | | return @as([*]u8, @ptrCast(node))[0 .. node.size & ~resize_bit]; |
| 270 | return @as([*]u8, @ptrCast(node))[0..node.size.toInt()]; |
| 261 | 271 | } |
| 262 | 272 | }; |
| 263 | 273 | |
| ... | ... | @@ -326,19 +336,22 @@ fn alloc(ctx: *anyopaque, n: usize, alignment: Alignment, ret_addr: usize) ?[*]u |
| 326 | 336 | retry: while (true) { |
| 327 | 337 | const first_node: ?*Node, const prev_size: usize = first_node: { |
| 328 | 338 | const node = cur_first_node orelse break :first_node .{ null, 0 }; |
| 329 | | var end_index = node.loadEndIndex(); |
| 330 | | while (true) { |
| 331 | | const buf = node.loadBuf(); |
| 332 | | const aligned_index = alignedIndex(buf.ptr, end_index, alignment); |
| 339 | const buf = node.loadBuf(); |
| 333 | 340 | |
| 334 | | if (aligned_index + n > buf.len) { |
| 335 | | break :first_node .{ node, buf.len }; |
| 336 | | } |
| 341 | // To avoid using a CAS loop in the hot path we atomically increase |
| 342 | // `end_index` by a large enough amount to be able to always provide |
| 343 | // the required alignment within the reserved memory. To recover the |
| 344 | // space this potentially wastes we try to subtract the 'overshoot' |
| 345 | // with a single cmpxchg afterwards, which may fail. |
| 337 | 346 | |
| 338 | | end_index = node.trySetEndIndex(end_index, aligned_index + n) orelse { |
| 339 | | return buf[aligned_index..][0..n].ptr; |
| 340 | | }; |
| 341 | | } |
| 347 | const alignable = n + alignment.toByteUnits() - 1; |
| 348 | const end_index = @atomicRmw(usize, &node.end_index, .Add, alignable, .monotonic); |
| 349 | const aligned_index = alignedIndex(buf.ptr, end_index, alignment); |
| 350 | assert(end_index + alignable >= aligned_index + n); |
| 351 | _ = @cmpxchgStrong(usize, &node.end_index, end_index + alignable, aligned_index + n, .monotonic, .monotonic); |
| 352 | |
| 353 | if (aligned_index + n > buf.len) break :first_node .{ node, buf.len }; |
| 354 | return buf[aligned_index..][0..n].ptr; |
| 342 | 355 | }; |
| 343 | 356 | |
| 344 | 357 | resize: { |
| ... | ... | @@ -352,7 +365,7 @@ fn alloc(ctx: *anyopaque, n: usize, alignment: Alignment, ret_addr: usize) ?[*]u |
| 352 | 365 | defer node.endResize(size); |
| 353 | 366 | |
| 354 | 367 | const buf = allocated_slice[@sizeOf(Node)..]; |
| 355 | | const end_index = node.loadEndIndex(); |
| 368 | const end_index = @atomicLoad(usize, &node.end_index, .monotonic); |
| 356 | 369 | const aligned_index = alignedIndex(buf.ptr, end_index, alignment); |
| 357 | 370 | const new_size = mem.alignForward(usize, @sizeOf(Node) + aligned_index + n, 2); |
| 358 | 371 | |
| ... | ... | @@ -403,55 +416,59 @@ fn alloc(ctx: *anyopaque, n: usize, alignment: Alignment, ret_addr: usize) ?[*]u |
| 403 | 416 | } |
| 404 | 417 | } |
| 405 | 418 | |
| 406 | | var best_fit_prev: ?*Node = null; |
| 407 | | var best_fit: ?*Node = null; |
| 408 | | var best_fit_diff: usize = std.math.maxInt(usize); |
| 409 | | |
| 410 | | var it_prev: ?*Node = null; |
| 411 | | var it = free_list; |
| 412 | | const candidate: ?*Node, const prev: ?*Node = find: while (it) |node| : ({ |
| 413 | | it_prev = it; |
| 414 | | it = node.next; |
| 415 | | }) { |
| 416 | | last_free = node; |
| 417 | | assert(node.size & Node.resize_bit == 0); |
| 418 | | const buf = node.allocatedSliceUnsafe()[@sizeOf(Node)..]; |
| 419 | | const aligned_index = alignedIndex(buf.ptr, 0, alignment); |
| 420 | | if (buf.len < aligned_index + n) { |
| 419 | const candidate: ?*Node, const prev: ?*Node = candidate: { |
| 420 | var best_fit_prev: ?*Node = null; |
| 421 | var best_fit: ?*Node = null; |
| 422 | var best_fit_diff: usize = std.math.maxInt(usize); |
| 423 | |
| 424 | var it_prev: ?*Node = null; |
| 425 | var it = free_list; |
| 426 | while (it) |node| : ({ |
| 427 | it_prev = it; |
| 428 | it = node.next; |
| 429 | }) { |
| 430 | last_free = node; |
| 431 | assert(!node.size.resizing); |
| 432 | const buf = node.allocatedSliceUnsafe()[@sizeOf(Node)..]; |
| 433 | const aligned_index = alignedIndex(buf.ptr, 0, alignment); |
| 434 | |
| 435 | if (aligned_index + n <= buf.len) { |
| 436 | break :candidate .{ node, it_prev }; |
| 437 | } |
| 438 | |
| 421 | 439 | const diff = aligned_index + n - buf.len; |
| 422 | 440 | if (diff <= best_fit_diff) { |
| 423 | 441 | best_fit_prev = it_prev; |
| 424 | 442 | best_fit = node; |
| 425 | 443 | best_fit_diff = diff; |
| 426 | 444 | } |
| 427 | | continue :find; |
| 428 | | } |
| 429 | | break :find .{ node, it_prev }; |
| 430 | | } else { |
| 431 | | // Ideally we want to use all nodes in `free_list` eventually, |
| 432 | | // so even if none fit we'll try to resize the one that was the |
| 433 | | // closest to being large enough. |
| 434 | | if (best_fit) |node| { |
| 435 | | const allocated_slice = node.allocatedSliceUnsafe(); |
| 436 | | const buf = allocated_slice[@sizeOf(Node)..]; |
| 437 | | const aligned_index = alignedIndex(buf.ptr, 0, alignment); |
| 438 | | const new_size = mem.alignForward(usize, @sizeOf(Node) + aligned_index + n, 2); |
| 439 | | |
| 440 | | if (arena.child_allocator.rawResize(allocated_slice, .of(Node), new_size, @returnAddress())) { |
| 441 | | node.size = new_size; |
| 442 | | break :find .{ node, best_fit_prev }; |
| 445 | } else { |
| 446 | // Ideally we want to use all nodes in `free_list` eventually, |
| 447 | // so even if none fit we'll try to resize the one that was the |
| 448 | // closest to being large enough. |
| 449 | if (best_fit) |node| { |
| 450 | const allocated_slice = node.allocatedSliceUnsafe(); |
| 451 | const buf = allocated_slice[@sizeOf(Node)..]; |
| 452 | const aligned_index = alignedIndex(buf.ptr, 0, alignment); |
| 453 | const new_size = mem.alignForward(usize, @sizeOf(Node) + aligned_index + n, 2); |
| 454 | |
| 455 | if (arena.child_allocator.rawResize(allocated_slice, .of(Node), new_size, @returnAddress())) { |
| 456 | node.size = .fromInt(new_size); |
| 457 | break :candidate .{ node, best_fit_prev }; |
| 458 | } |
| 443 | 459 | } |
| 460 | break :from_free_list; |
| 444 | 461 | } |
| 445 | | break :from_free_list; |
| 446 | 462 | }; |
| 447 | 463 | |
| 448 | | it = last_free; |
| 449 | | while (it) |node| : (it = node.next) { |
| 450 | | last_free = node; |
| 464 | { |
| 465 | var it = last_free; |
| 466 | while (it) |node| : (it = node.next) { |
| 467 | last_free = node; |
| 468 | } |
| 451 | 469 | } |
| 452 | 470 | |
| 453 | 471 | const node = candidate orelse break :from_free_list; |
| 454 | | |
| 455 | 472 | const old_next = node.next; |
| 456 | 473 | |
| 457 | 474 | const buf = node.allocatedSliceUnsafe()[@sizeOf(Node)..]; |
| ... | ... | @@ -489,12 +506,11 @@ fn alloc(ctx: *anyopaque, n: usize, alignment: Alignment, ret_addr: usize) ?[*]u |
| 489 | 506 | const big_enough_size = prev_size + min_size + 16; |
| 490 | 507 | break :size mem.alignForward(usize, big_enough_size + big_enough_size / 2, 2); |
| 491 | 508 | }; |
| 492 | | assert(size & Node.resize_bit == 0); |
| 493 | 509 | const ptr = arena.child_allocator.rawAlloc(size, .of(Node), @returnAddress()) orelse |
| 494 | 510 | return null; |
| 495 | 511 | const new_node: *Node = @ptrCast(@alignCast(ptr)); |
| 496 | 512 | new_node.* = .{ |
| 497 | | .size = size, |
| 513 | .size = .fromInt(size), |
| 498 | 514 | .end_index = undefined, // set below |
| 499 | 515 | .next = undefined, // set below |
| 500 | 516 | }; |
| ... | ... | @@ -504,7 +520,7 @@ fn alloc(ctx: *anyopaque, n: usize, alignment: Alignment, ret_addr: usize) ?[*]u |
| 504 | 520 | |
| 505 | 521 | const buf = new_node.allocatedSliceUnsafe()[@sizeOf(Node)..]; |
| 506 | 522 | const aligned_index = alignedIndex(buf.ptr, 0, alignment); |
| 507 | | assert(new_node.size >= @sizeOf(Node) + aligned_index + n); |
| 523 | assert(new_node.size.toInt() >= @sizeOf(Node) + aligned_index + n); |
| 508 | 524 | |
| 509 | 525 | new_node.end_index = aligned_index + n; |
| 510 | 526 | new_node.next = first_node; |
| ... | ... | @@ -533,7 +549,7 @@ fn resize(ctx: *anyopaque, buf: []u8, alignment: Alignment, new_len: usize, ret_ |
| 533 | 549 | const node = arena.loadFirstNode().?; |
| 534 | 550 | const cur_buf_ptr = @as([*]u8, @ptrCast(node)) + @sizeOf(Node); |
| 535 | 551 | |
| 536 | | var cur_end_index = node.loadEndIndex(); |
| 552 | var cur_end_index = @atomicLoad(usize, &node.end_index, .monotonic); |
| 537 | 553 | while (true) { |
| 538 | 554 | if (cur_buf_ptr + cur_end_index != buf.ptr + buf.len) { |
| 539 | 555 | // It's not the most recent allocation, so it cannot be expanded, |
| ... | ... | @@ -554,7 +570,14 @@ fn resize(ctx: *anyopaque, buf: []u8, alignment: Alignment, new_len: usize, ret_ |
| 554 | 570 | return false; |
| 555 | 571 | }; |
| 556 | 572 | |
| 557 | | cur_end_index = node.trySetEndIndex(cur_end_index, new_end_index) orelse { |
| 573 | cur_end_index = @cmpxchgWeak( |
| 574 | usize, |
| 575 | &node.end_index, |
| 576 | cur_end_index, |
| 577 | new_end_index, |
| 578 | .monotonic, |
| 579 | .monotonic, |
| 580 | ) orelse { |
| 558 | 581 | return true; |
| 559 | 582 | }; |
| 560 | 583 | } |
| ... | ... | @@ -580,14 +603,22 @@ fn free(ctx: *anyopaque, buf: []u8, alignment: Alignment, ret_addr: usize) void |
| 580 | 603 | const node = arena.loadFirstNode().?; |
| 581 | 604 | const cur_buf_ptr: [*]u8 = @as([*]u8, @ptrCast(node)) + @sizeOf(Node); |
| 582 | 605 | |
| 583 | | var cur_end_index = node.loadEndIndex(); |
| 606 | var cur_end_index = @atomicLoad(usize, &node.end_index, .monotonic); |
| 584 | 607 | while (true) { |
| 585 | 608 | if (cur_buf_ptr + cur_end_index != buf.ptr + buf.len) { |
| 586 | 609 | // Not the most recent allocation; we cannot free it. |
| 587 | 610 | return; |
| 588 | 611 | } |
| 589 | 612 | const new_end_index = cur_end_index - buf.len; |
| 590 | | cur_end_index = node.trySetEndIndex(cur_end_index, new_end_index) orelse { |
| 613 | |
| 614 | cur_end_index = @cmpxchgWeak( |
| 615 | usize, |
| 616 | &node.end_index, |
| 617 | cur_end_index, |
| 618 | new_end_index, |
| 619 | .monotonic, |
| 620 | .monotonic, |
| 621 | ) orelse { |
| 591 | 622 | return; |
| 592 | 623 | }; |
| 593 | 624 | } |
| ... | ... | @@ -637,6 +668,7 @@ test "reset while retaining a buffer" { |
| 637 | 668 | try std.testing.expect(arena_allocator.state.used_list.?.next != null); |
| 638 | 669 | |
| 639 | 670 | // This retains the first allocated buffer |
| 640 | | try std.testing.expect(arena_allocator.reset(.{ .retain_with_limit = 1 })); |
| 671 | try std.testing.expect(arena_allocator.reset(.{ .retain_with_limit = 2 })); |
| 641 | 672 | try std.testing.expect(arena_allocator.state.used_list.?.next == null); |
| 673 | try std.testing.expectEqual(2, arena_allocator.queryCapacity()); |
| 642 | 674 | } |