mirror of
https://codeberg.org/ziglang/zig.git
synced 2026-04-30 14:52:41 +03:00
ce3f254526
The cmpxchg is there to recover alignment padding that isn't needed (which can only be determined after the fetch-and-add that reserves it as allocated memory). As cmpxchg tends to be a very expensive operation, it is actually faster to introduce an additional branch here that checks if the cmpxchg would be a noop (because all of the reserved alignment padding was in fact necessary) and skips it if that's the case. This does not measurably regress performance if the arena is only accessed by a single thread and yields slight performance benefits for multi-threaded usage. If the arena is commonly used for unaligned allocations, the perf benefits are quite significant. Co-authored-by: Jacob Young <amazingjacob@gmail.com>
1078 lines
40 KiB
Zig
1078 lines
40 KiB
Zig
//! This allocator takes an existing allocator, wraps it, and provides an interface where
|
|
//! you can allocate and then free it all together. Calls to free an individual item only
|
|
//! free the item if it was the most recent allocation, otherwise calls to free do
|
|
//! nothing.
|
|
//!
|
|
//! The `Allocator` implementation provided is threadsafe, given that `child_allocator`
|
|
//! is threadsafe as well.
|
|
const ArenaAllocator = @This();
|
|
|
|
child_allocator: Allocator,
|
|
state: State,
|
|
|
|
/// Inner state of ArenaAllocator. Can be stored rather than the entire ArenaAllocator
|
|
/// as a memory-saving optimization.
|
|
///
|
|
/// Default initialization of this struct is deprecated; use `init` instead.
|
|
pub const State = struct {
|
|
used_list: ?*Node = null,
|
|
free_list: ?*Node = null,
|
|
|
|
pub const init: State = .{
|
|
.used_list = null,
|
|
.free_list = null,
|
|
};
|
|
|
|
pub fn promote(state: State, child_allocator: Allocator) ArenaAllocator {
|
|
return .{
|
|
.child_allocator = child_allocator,
|
|
.state = state,
|
|
};
|
|
}
|
|
};
|
|
|
|
pub fn allocator(arena: *ArenaAllocator) Allocator {
|
|
return .{
|
|
.ptr = arena,
|
|
.vtable = &.{
|
|
.alloc = alloc,
|
|
.resize = resize,
|
|
.remap = remap,
|
|
.free = free,
|
|
},
|
|
};
|
|
}
|
|
|
|
pub fn init(child_allocator: Allocator) ArenaAllocator {
|
|
return State.init.promote(child_allocator);
|
|
}
|
|
|
|
/// Not threadsafe.
|
|
pub fn deinit(arena: ArenaAllocator) void {
|
|
// NOTE: When changing this, make sure `reset()` is adjusted accordingly!
|
|
|
|
for ([_]?*Node{ arena.state.used_list, arena.state.free_list }) |first_node| {
|
|
var it = first_node;
|
|
while (it) |node| {
|
|
// this has to occur before the free because the free frees node
|
|
it = node.next;
|
|
arena.child_allocator.rawFree(node.allocatedSliceUnsafe(), .of(Node), @returnAddress());
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Queries the current memory use of this arena.
|
|
/// This will **not** include the storage required for internal keeping.
|
|
///
|
|
/// Not threadsafe.
|
|
pub fn queryCapacity(arena: ArenaAllocator) usize {
|
|
var capacity: usize = 0;
|
|
for ([_]?*Node{ arena.state.used_list, arena.state.free_list }) |first_node| {
|
|
capacity += countListCapacity(first_node);
|
|
}
|
|
return capacity;
|
|
}
|
|
fn countListCapacity(first_node: ?*Node) usize {
|
|
var capacity: usize = 0;
|
|
var it = first_node;
|
|
while (it) |node| : (it = node.next) {
|
|
// Compute the actually allocated size excluding the
|
|
// linked list node.
|
|
capacity += node.size.toInt() - @sizeOf(Node);
|
|
}
|
|
return capacity;
|
|
}
|
|
|
|
pub const ResetMode = union(enum) {
|
|
/// Releases all allocated memory in the arena.
|
|
free_all,
|
|
/// This will pre-heat the arena for future allocations by allocating a
|
|
/// large enough buffer for all previously done allocations.
|
|
/// Preheating will speed up the allocation process by invoking the backing allocator
|
|
/// less often than before. If `reset()` is used in a loop, this means that after the
|
|
/// biggest operation, no memory allocations are performed anymore.
|
|
retain_capacity,
|
|
/// This is the same as `retain_capacity`, but the memory will be shrunk to
|
|
/// this value if it exceeds the limit.
|
|
retain_with_limit: usize,
|
|
};
|
|
/// Resets the arena allocator and frees all allocated memory.
|
|
///
|
|
/// `mode` defines how the currently allocated memory is handled.
|
|
/// See the variant documentation for `ResetMode` for the effects of each mode.
|
|
///
|
|
/// The function will return whether the reset operation was successful or not.
|
|
/// If the reallocation failed `false` is returned. The arena will still be fully
|
|
/// functional in that case, all memory is released. Future allocations just might
|
|
/// be slower.
|
|
///
|
|
/// Not threadsafe.
|
|
///
|
|
/// NOTE: If `mode` is `free_all`, the function will always return `true`.
|
|
pub fn reset(arena: *ArenaAllocator, mode: ResetMode) bool {
|
|
// Some words on the implementation:
|
|
// The reset function can be implemented with two basic approaches:
|
|
// - Counting how much bytes were allocated since the last reset, and storing that
|
|
// information in State. This will make reset fast and alloc only a teeny tiny bit
|
|
// slower.
|
|
// - Counting how much bytes were allocated by iterating the chunk linked list. This
|
|
// will make reset slower, but alloc() keeps the same speed when reset() as if reset()
|
|
// would not exist.
|
|
//
|
|
// The second variant was chosen for implementation, as with more and more calls to reset(),
|
|
// the function will get faster and faster. At one point, the complexity of the function
|
|
// will drop to amortized O(1), as we're only ever having a single chunk that will not be
|
|
// reallocated, and we're not even touching the backing allocator anymore.
|
|
//
|
|
// Thus, only the first hand full of calls to reset() will actually need to iterate the linked
|
|
// list, all future calls are just taking the first node, and only resetting the `end_index`
|
|
// value.
|
|
|
|
const limit: ?usize = switch (mode) {
|
|
.retain_capacity => null,
|
|
.retain_with_limit => |limit| limit,
|
|
.free_all => 0,
|
|
};
|
|
if (limit == 0) {
|
|
// just reset when we don't have anything to reallocate
|
|
arena.deinit();
|
|
arena.state = .init;
|
|
return true;
|
|
}
|
|
|
|
const used_capacity = countListCapacity(arena.state.used_list);
|
|
const free_capacity = countListCapacity(arena.state.free_list);
|
|
|
|
const new_used_capacity = if (limit) |lim| @min(lim, used_capacity) else used_capacity;
|
|
const new_free_capacity = if (limit) |lim| @min(lim - new_used_capacity, free_capacity) else free_capacity;
|
|
|
|
var ok = true;
|
|
|
|
for (
|
|
[_]*?*Node{ &arena.state.used_list, &arena.state.free_list },
|
|
[_]usize{ new_used_capacity, new_free_capacity },
|
|
) |first_node_ptr, new_capacity| {
|
|
// Free all nodes except for the last one
|
|
var it = first_node_ptr.*;
|
|
const node: *Node = while (it) |node| {
|
|
// this has to occur before the free because the free frees node
|
|
it = node.next;
|
|
if (it == null) break node;
|
|
arena.child_allocator.rawFree(node.allocatedSliceUnsafe(), .of(Node), @returnAddress());
|
|
} else {
|
|
continue;
|
|
};
|
|
const allocated_slice = node.allocatedSliceUnsafe();
|
|
|
|
// Align backwards to always stay below limit.
|
|
const new_size = mem.alignBackward(usize, @sizeOf(Node) + new_capacity, 2);
|
|
|
|
if (new_size == @sizeOf(Node)) {
|
|
arena.child_allocator.rawFree(allocated_slice, .of(Node), @returnAddress());
|
|
first_node_ptr.* = null;
|
|
continue;
|
|
}
|
|
|
|
node.end_index = 0;
|
|
first_node_ptr.* = node;
|
|
|
|
if (allocated_slice.len == new_size) {
|
|
// perfect, no need to invoke the child_allocator
|
|
continue;
|
|
}
|
|
|
|
if (arena.child_allocator.rawResize(allocated_slice, .of(Node), new_size, @returnAddress())) {
|
|
// successful resize
|
|
node.size = .fromInt(new_size);
|
|
} else {
|
|
// manual realloc
|
|
const new_ptr = arena.child_allocator.rawAlloc(new_size, .of(Node), @returnAddress()) orelse {
|
|
// we failed to preheat the arena properly, signal this to the user.
|
|
ok = false;
|
|
continue;
|
|
};
|
|
arena.child_allocator.rawFree(allocated_slice, .of(Node), @returnAddress());
|
|
const new_first_node: *Node = @ptrCast(@alignCast(new_ptr));
|
|
new_first_node.* = .{
|
|
.size = .fromInt(new_size),
|
|
.end_index = 0,
|
|
.next = null,
|
|
};
|
|
first_node_ptr.* = new_first_node;
|
|
}
|
|
}
|
|
|
|
return ok;
|
|
}
|
|
|
|
/// Concurrent accesses to node pointers generally have to have acquire/release
|
|
/// semantics to guarantee that newly allocated notes are in a valid state when
|
|
/// being inserted into a list. Exceptions are possible, e.g. a cmpxchg loop that
|
|
/// never accesses the node returned on failure can use monotonic semantics on
|
|
/// failure, but must still use release semantics on success to protect the node
|
|
/// it's trying to push.
|
|
const Node = struct {
|
|
/// Only meant to be accessed indirectly via the methods supplied by this type,
|
|
/// except if the node is owned by the thread accessing it.
|
|
/// Must always be an even number to accommodate `resize` bit.
|
|
size: Size,
|
|
/// Any increase of `end_index` has to use acquire semantics;
|
|
/// any decrease of `end_index` that invalidates (formerly) active allocations
|
|
/// has to use release semantics.
|
|
/// This guarantees that all accesses to memory that's about to be freed
|
|
/// happen-before the free is published.
|
|
/// Since `size` can only grow and never shrink, memory access depending on
|
|
/// any `end_index` <= any `size` can never be OOB.
|
|
end_index: usize,
|
|
/// This field should only be accessed if the node is owned by the thread
|
|
/// accessing it.
|
|
next: ?*Node,
|
|
|
|
const Size = packed struct(usize) {
|
|
resizing: bool,
|
|
_: @Int(.unsigned, @bitSizeOf(usize) - 1) = 0,
|
|
|
|
fn fromInt(int: usize) Size {
|
|
assert(int >= @sizeOf(Node));
|
|
const size: Size = @bitCast(int);
|
|
assert(!size.resizing);
|
|
return size;
|
|
}
|
|
|
|
fn toInt(size: Size) usize {
|
|
var int = size;
|
|
int.resizing = false;
|
|
return @bitCast(int);
|
|
}
|
|
|
|
comptime {
|
|
assert(Size{ .resizing = true } == @as(Size, @bitCast(@as(usize, 1))));
|
|
}
|
|
};
|
|
|
|
fn loadBuf(node: *Node) []u8 {
|
|
// `size` can only ever grow, so the buffer returned by this function is
|
|
// always valid memory.
|
|
const size = @atomicLoad(Size, &node.size, .monotonic);
|
|
return @as([*]u8, @ptrCast(node))[0..size.toInt()][@sizeOf(Node)..];
|
|
}
|
|
|
|
/// Returns allocated slice or `null` if node is already (being) resized.
|
|
fn beginResize(node: *Node) ?[]u8 {
|
|
const size = @atomicRmw(Size, &node.size, .Or, .{ .resizing = true }, .acquire); // syncs with release in `endResize`
|
|
if (size.resizing) return null;
|
|
return @as([*]u8, @ptrCast(node))[0..size.toInt()];
|
|
}
|
|
|
|
fn endResize(node: *Node, size: usize, prev_size: usize) void {
|
|
assert(size >= prev_size); // nodes must not shrink
|
|
assert(@atomicLoad(Size, &node.size, .unordered).toInt() == prev_size);
|
|
return @atomicStore(Size, &node.size, .fromInt(size), .release); // syncs with acquire in `beginResize`
|
|
}
|
|
|
|
/// Not threadsafe.
|
|
fn allocatedSliceUnsafe(node: *Node) []u8 {
|
|
return @as([*]u8, @ptrCast(node))[0..node.size.toInt()];
|
|
}
|
|
};
|
|
|
|
fn loadFirstNode(arena: *ArenaAllocator) ?*Node {
|
|
return @atomicLoad(?*Node, &arena.state.used_list, .acquire); // syncs with release in successful `tryPushNode`
|
|
}
|
|
|
|
const PushResult = union(enum) {
|
|
success,
|
|
failure: ?*Node,
|
|
};
|
|
fn tryPushNode(arena: *ArenaAllocator, node: *Node) PushResult {
|
|
assert(node != node.next);
|
|
if (@cmpxchgStrong( // strong because retrying means discarding a fitting node -> expensive
|
|
?*Node,
|
|
&arena.state.used_list,
|
|
node.next,
|
|
node,
|
|
.release, // syncs with acquire in failure path or `loadFirstNode`
|
|
.acquire, // syncs with release in success path
|
|
)) |old_node| {
|
|
return .{ .failure = old_node };
|
|
} else {
|
|
return .success;
|
|
}
|
|
}
|
|
|
|
fn stealFreeList(arena: *ArenaAllocator) ?*Node {
|
|
// We don't need acq_rel here because we're always swapping in `null`, so
|
|
// there's no node we'd need to release.
|
|
return @atomicRmw(?*Node, &arena.state.free_list, .Xchg, null, .acquire); // syncs with release in `pushFreeList`
|
|
}
|
|
|
|
fn pushFreeList(arena: *ArenaAllocator, first: *Node, last: *Node) void {
|
|
assert(first != last.next);
|
|
assert(first != first.next);
|
|
assert(last != last.next);
|
|
while (@cmpxchgWeak(
|
|
?*Node,
|
|
&arena.state.free_list,
|
|
last.next,
|
|
first,
|
|
.release, // syncs with acquire in `stealFreeList`
|
|
.monotonic, // we never access any fields of `old_free_list`, we only care about the pointer
|
|
)) |old_free_list| {
|
|
last.next = old_free_list;
|
|
}
|
|
}
|
|
|
|
fn alignedIndex(buf_ptr: [*]u8, end_index: usize, alignment: Alignment) usize {
|
|
// Wrapping arithmetic to avoid overflows since `end_index` isn't bounded by
|
|
// `size`. This is always ok since the max alignment in byte units is also
|
|
// the max value of `usize` so wrapped values are correctly aligned anyway.
|
|
return alignment.forward(@intFromPtr(buf_ptr) +% end_index) -% @intFromPtr(buf_ptr);
|
|
}
|
|
|
|
fn alloc(ctx: *anyopaque, n: usize, alignment: Alignment, ret_addr: usize) ?[*]u8 {
|
|
const arena: *ArenaAllocator = @ptrCast(@alignCast(ctx));
|
|
_ = ret_addr;
|
|
|
|
assert(n > 0);
|
|
|
|
var cur_first_node = arena.loadFirstNode();
|
|
|
|
var cur_new_node: ?*Node = null;
|
|
defer if (cur_new_node) |node| {
|
|
node.next = null; // optimize for empty free list
|
|
arena.pushFreeList(node, node);
|
|
};
|
|
|
|
retry: while (true) {
|
|
const first_node: ?*Node, const prev_size: usize = first_node: {
|
|
const node = cur_first_node orelse break :first_node .{ null, 0 };
|
|
const buf = node.loadBuf();
|
|
|
|
// To avoid using a CAS loop in the hot path we atomically increase
|
|
// `end_index` by a large enough amount to be able to always provide
|
|
// the required alignment within the reserved memory. To recover the
|
|
// space this potentially wastes we try to subtract the 'overshoot'
|
|
// with a single cmpxchg afterwards, which may fail.
|
|
|
|
const alignable = n + alignment.toByteUnits() - 1;
|
|
const end_index = @atomicRmw(usize, &node.end_index, .Add, alignable, .acquire); // acquire any memory that may have been freed
|
|
const aligned_index = alignedIndex(buf.ptr, end_index, alignment);
|
|
assert(end_index + alignable >= aligned_index + n);
|
|
if (end_index + alignable != aligned_index + n) {
|
|
_ = @cmpxchgStrong(
|
|
usize,
|
|
&node.end_index,
|
|
end_index + alignable,
|
|
aligned_index + n,
|
|
.monotonic, // no need to release alignment padding; there's no one accessing it!
|
|
.monotonic,
|
|
);
|
|
}
|
|
|
|
if (aligned_index + n > buf.len) break :first_node .{ node, buf.len };
|
|
return buf[aligned_index..][0..n].ptr;
|
|
};
|
|
|
|
resize: {
|
|
// Before attempting to get our hands on a new node, we try to resize
|
|
// the one we're currently holding. This is an exclusive operation;
|
|
// if another thread is already in this section we can never resize.
|
|
|
|
const node = first_node orelse break :resize;
|
|
const allocated_slice = node.beginResize() orelse break :resize;
|
|
var size = allocated_slice.len;
|
|
defer node.endResize(size, allocated_slice.len);
|
|
|
|
const buf = allocated_slice[@sizeOf(Node)..];
|
|
const end_index = @atomicLoad(usize, &node.end_index, .monotonic);
|
|
const aligned_index = alignedIndex(buf.ptr, end_index, alignment);
|
|
const new_size = mem.alignForward(usize, @sizeOf(Node) + aligned_index + n, 2);
|
|
|
|
if (new_size <= allocated_slice.len) {
|
|
// A `resize` or `free` call managed to sneak in and we need to
|
|
// guarantee that `size` is only ever increased; retry!
|
|
continue :retry;
|
|
}
|
|
|
|
if (arena.child_allocator.rawResize(allocated_slice, .of(Node), new_size, @returnAddress())) {
|
|
size = new_size;
|
|
|
|
// strong because a spurious failure could result in suboptimal
|
|
// usage of this node
|
|
if (null == @cmpxchgStrong(
|
|
usize,
|
|
&node.end_index,
|
|
end_index,
|
|
aligned_index + n,
|
|
.acquire, // acquire any memory that may have been freed
|
|
.monotonic,
|
|
)) {
|
|
const new_buf = allocated_slice.ptr[0..new_size][@sizeOf(Node)..];
|
|
return new_buf[aligned_index..][0..n].ptr;
|
|
}
|
|
}
|
|
}
|
|
|
|
// We need a new node! First, we search `free_list` for one that's big
|
|
// enough, if we don't find one there we fall back to allocating a new
|
|
// node with `child_allocator` (if we haven't already done that!).
|
|
|
|
from_free_list: {
|
|
// We 'steal' the entire free list to operate on it without other
|
|
// threads getting up into our business.
|
|
// This is a rather pragmatic approach, but since the free list isn't
|
|
// used very frequently it's fine performance-wise, even under load.
|
|
// Also this avoids the ABA problem; stealing the list with an atomic
|
|
// swap doesn't introduce any potentially stale `next` pointers.
|
|
|
|
const free_list = arena.stealFreeList() orelse break :from_free_list;
|
|
|
|
const first_free: *Node, const last_free: *Node, const node: *Node, const prev: ?*Node = find: {
|
|
var best_fit_prev: ?*Node = null;
|
|
var best_fit: ?*Node = null;
|
|
var best_fit_diff: usize = std.math.maxInt(usize);
|
|
|
|
var it_prev: ?*Node = null;
|
|
var it: ?*Node = free_list;
|
|
while (it) |node| : ({
|
|
it_prev = node;
|
|
it = node.next;
|
|
}) {
|
|
assert(!node.size.resizing);
|
|
const buf = node.allocatedSliceUnsafe()[@sizeOf(Node)..];
|
|
const aligned_index = alignedIndex(buf.ptr, 0, alignment);
|
|
|
|
const diff = aligned_index + n -| buf.len;
|
|
if (diff < best_fit_diff) {
|
|
best_fit_prev = it_prev;
|
|
best_fit = node;
|
|
best_fit_diff = diff;
|
|
}
|
|
}
|
|
|
|
break :find .{ free_list, it_prev.?, best_fit.?, best_fit_prev };
|
|
};
|
|
|
|
const aligned_index, const need_resize = aligned_index: {
|
|
const buf = node.allocatedSliceUnsafe()[@sizeOf(Node)..];
|
|
const aligned_index = alignedIndex(buf.ptr, 0, alignment);
|
|
break :aligned_index .{ aligned_index, aligned_index + n > buf.len };
|
|
};
|
|
|
|
if (need_resize) {
|
|
// Ideally we want to use all nodes in `free_list` eventually,
|
|
// so even if none fit we'll try to resize the one that was the
|
|
// closest to being large enough.
|
|
const new_size = mem.alignForward(usize, @sizeOf(Node) + aligned_index + n, 2);
|
|
if (arena.child_allocator.rawResize(node.allocatedSliceUnsafe(), .of(Node), new_size, @returnAddress())) {
|
|
node.size = .fromInt(new_size);
|
|
} else {
|
|
arena.pushFreeList(first_free, last_free);
|
|
break :from_free_list; // we couldn't find a fitting free node
|
|
}
|
|
}
|
|
|
|
const buf = node.allocatedSliceUnsafe()[@sizeOf(Node)..];
|
|
const old_next = node.next;
|
|
|
|
node.end_index = aligned_index + n;
|
|
node.next = first_node;
|
|
|
|
switch (arena.tryPushNode(node)) {
|
|
.success => {
|
|
// Finish removing node from free list.
|
|
if (prev) |p| p.next = old_next;
|
|
|
|
// Push remaining stolen free list back onto `arena.state.free_list`.
|
|
const new_first_free = if (node == first_free) old_next else first_free;
|
|
const new_last_free = if (node == last_free) prev else last_free;
|
|
if (new_first_free) |first| {
|
|
const last = new_last_free.?;
|
|
arena.pushFreeList(first, last);
|
|
}
|
|
|
|
return buf[aligned_index..][0..n].ptr;
|
|
},
|
|
.failure => |old_first_node| {
|
|
// restore free list to as we found it
|
|
node.next = old_next;
|
|
arena.pushFreeList(first_free, last_free);
|
|
|
|
cur_first_node = old_first_node;
|
|
continue :retry; // there's a new first node; retry!
|
|
},
|
|
}
|
|
}
|
|
|
|
const new_node: *Node = new_node: {
|
|
if (cur_new_node) |new_node| {
|
|
break :new_node new_node;
|
|
} else {
|
|
@branchHint(.cold);
|
|
}
|
|
|
|
const size: Node.Size = size: {
|
|
const min_size = @sizeOf(Node) + alignment.toByteUnits() + n;
|
|
const big_enough_size = prev_size + min_size + 16;
|
|
const size = mem.alignForward(usize, big_enough_size + big_enough_size / 2, 2);
|
|
break :size .fromInt(size);
|
|
};
|
|
const ptr = arena.child_allocator.rawAlloc(size.toInt(), .of(Node), @returnAddress()) orelse
|
|
return null;
|
|
const new_node: *Node = @ptrCast(@alignCast(ptr));
|
|
new_node.* = .{
|
|
.size = size,
|
|
.end_index = undefined, // set below
|
|
.next = undefined, // set below
|
|
};
|
|
cur_new_node = new_node;
|
|
break :new_node new_node;
|
|
};
|
|
|
|
const buf = new_node.allocatedSliceUnsafe()[@sizeOf(Node)..];
|
|
const aligned_index = alignedIndex(buf.ptr, 0, alignment);
|
|
assert(new_node.size.toInt() >= @sizeOf(Node) + aligned_index + n);
|
|
|
|
new_node.end_index = aligned_index + n;
|
|
new_node.next = first_node;
|
|
|
|
switch (arena.tryPushNode(new_node)) {
|
|
.success => {
|
|
cur_new_node = null;
|
|
return buf[aligned_index..][0..n].ptr;
|
|
},
|
|
.failure => |old_first_node| {
|
|
cur_first_node = old_first_node;
|
|
},
|
|
}
|
|
}
|
|
}
|
|
|
|
fn resize(ctx: *anyopaque, memory: []u8, alignment: Alignment, new_len: usize, ret_addr: usize) bool {
|
|
const arena: *ArenaAllocator = @ptrCast(@alignCast(ctx));
|
|
_ = alignment;
|
|
_ = ret_addr;
|
|
|
|
assert(memory.len > 0);
|
|
assert(new_len > 0);
|
|
|
|
const node = arena.loadFirstNode().?;
|
|
const buf_ptr = @as([*]u8, @ptrCast(node)) + @sizeOf(Node);
|
|
|
|
const cur_end_index = @atomicLoad(usize, &node.end_index, .monotonic);
|
|
|
|
if (buf_ptr + cur_end_index != memory.ptr + memory.len) {
|
|
// It's not the most recent allocation, so it cannot be expanded,
|
|
// but it's fine if they want to make it smaller.
|
|
return new_len <= memory.len;
|
|
}
|
|
|
|
if (new_len <= memory.len) {
|
|
const new_end_index = cur_end_index - (memory.len - new_len);
|
|
assert(buf_ptr + new_end_index == memory.ptr + new_len);
|
|
|
|
_ = @cmpxchgStrong(
|
|
usize,
|
|
&node.end_index,
|
|
cur_end_index,
|
|
new_end_index,
|
|
.release, // release freed memory
|
|
.monotonic,
|
|
);
|
|
return true; // Shrinking allocations should always succeed.
|
|
}
|
|
|
|
// Saturating arithmetic because `end_index` is not guaranteed to be `<= size`.
|
|
// The allocation we're trying to resize *could* belong to a different node!
|
|
if (node.loadBuf().len -| cur_end_index >= new_len - memory.len) {
|
|
const new_end_index = cur_end_index + (new_len - memory.len);
|
|
assert(buf_ptr + new_end_index == memory.ptr + new_len);
|
|
|
|
return null == @cmpxchgStrong(
|
|
usize,
|
|
&node.end_index,
|
|
cur_end_index,
|
|
new_end_index,
|
|
.acquire, // acquire any memory that may have been freed
|
|
.monotonic,
|
|
);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
fn remap(ctx: *anyopaque, memory: []u8, alignment: Alignment, new_len: usize, ret_addr: usize) ?[*]u8 {
|
|
return if (resize(ctx, memory, alignment, new_len, ret_addr)) memory.ptr else null;
|
|
}
|
|
|
|
fn free(ctx: *anyopaque, memory: []u8, alignment: Alignment, ret_addr: usize) void {
|
|
const arena: *ArenaAllocator = @ptrCast(@alignCast(ctx));
|
|
_ = alignment;
|
|
_ = ret_addr;
|
|
|
|
assert(memory.len > 0);
|
|
|
|
const node = arena.loadFirstNode().?;
|
|
const buf_ptr = @as([*]u8, @ptrCast(node)) + @sizeOf(Node);
|
|
|
|
const cur_end_index = @atomicLoad(usize, &node.end_index, .monotonic);
|
|
|
|
if (buf_ptr + cur_end_index != memory.ptr + memory.len) {
|
|
// Not the most recent allocation; we cannot free it.
|
|
return;
|
|
}
|
|
|
|
const new_end_index = cur_end_index - memory.len;
|
|
assert(buf_ptr + new_end_index == memory.ptr);
|
|
|
|
_ = @cmpxchgStrong(
|
|
usize,
|
|
&node.end_index,
|
|
cur_end_index,
|
|
new_end_index,
|
|
.release, // release freed memory
|
|
.monotonic,
|
|
);
|
|
}
|
|
|
|
const std = @import("std");
|
|
const assert = std.debug.assert;
|
|
const mem = std.mem;
|
|
const Allocator = std.mem.Allocator;
|
|
const Alignment = std.mem.Alignment;
|
|
|
|
test "reset with preheating" {
|
|
var arena_allocator = ArenaAllocator.init(std.testing.allocator);
|
|
defer arena_allocator.deinit();
|
|
// provides some variance in the allocated data
|
|
var rng_src = std.Random.DefaultPrng.init(std.testing.random_seed);
|
|
const random = rng_src.random();
|
|
var rounds: usize = 25;
|
|
while (rounds > 0) {
|
|
rounds -= 1;
|
|
_ = arena_allocator.reset(.retain_capacity);
|
|
var alloced_bytes: usize = 0;
|
|
const total_size: usize = random.intRangeAtMost(usize, 256, 16384);
|
|
while (alloced_bytes < total_size) {
|
|
const size = random.intRangeAtMost(usize, 16, 256);
|
|
const alignment: Alignment = .@"32";
|
|
const slice = try arena_allocator.allocator().alignedAlloc(u8, alignment, size);
|
|
try std.testing.expect(alignment.check(@intFromPtr(slice.ptr)));
|
|
try std.testing.expectEqual(size, slice.len);
|
|
alloced_bytes += slice.len;
|
|
}
|
|
}
|
|
}
|
|
|
|
test "reset while retaining a buffer" {
|
|
var arena_allocator = ArenaAllocator.init(std.testing.allocator);
|
|
defer arena_allocator.deinit();
|
|
const a = arena_allocator.allocator();
|
|
|
|
// Create two internal buffers
|
|
_ = try a.alloc(u8, 1);
|
|
_ = try a.alloc(u8, 1000);
|
|
|
|
try std.testing.expect(arena_allocator.state.used_list != null);
|
|
|
|
// Check that we have at least two buffers
|
|
try std.testing.expect(arena_allocator.state.used_list.?.next != null);
|
|
|
|
// This retains the first allocated buffer
|
|
try std.testing.expect(arena_allocator.reset(.{ .retain_with_limit = 2 }));
|
|
try std.testing.expect(arena_allocator.state.used_list.?.next == null);
|
|
try std.testing.expectEqual(2, arena_allocator.queryCapacity());
|
|
}
|
|
|
|
test "fuzz multi threaded" {
|
|
@disableInstrumentation();
|
|
if (@import("builtin").single_threaded) return error.SkipZigTest;
|
|
|
|
const gpa = std.heap.smp_allocator;
|
|
|
|
var io_instance: std.Io.Threaded = .init(gpa, .{});
|
|
defer io_instance.deinit();
|
|
|
|
var arena_state: ArenaAllocator.State = .init;
|
|
// No need to deinit arena_state, all allocations are in `sample_buffer`!
|
|
|
|
const buffer_size = FuzzContext.max_alloc_count * FuzzContext.max_alloc_size;
|
|
|
|
const control_buffer = try gpa.alloc(u8, buffer_size);
|
|
defer gpa.free(control_buffer);
|
|
var control_instance: std.heap.FixedBufferAllocator = .init(control_buffer);
|
|
|
|
const sample_buffer = try gpa.alloc(u8, buffer_size);
|
|
defer gpa.free(sample_buffer);
|
|
var sample_instance: FuzzAllocator = .init(sample_buffer);
|
|
|
|
try std.testing.fuzz(FuzzContext.Init{
|
|
.threaded_instance = &io_instance,
|
|
.arena_state = &arena_state,
|
|
.control_instance = &control_instance,
|
|
.sample_instance = &sample_instance,
|
|
}, fuzzMultiThreaded, .{});
|
|
}
|
|
|
|
fn fuzzMultiThreaded(fuzz_init: FuzzContext.Init, smith: *std.testing.Smith) anyerror!void {
|
|
@disableInstrumentation();
|
|
const testing = std.testing;
|
|
const io = fuzz_init.threaded_instance.io();
|
|
|
|
fuzz_init.sample_instance.prepareFailures(smith);
|
|
|
|
const control_allocator = fuzz_init.control_instance.threadSafeAllocator();
|
|
const sample_child_allocator = fuzz_init.sample_instance.allocator();
|
|
|
|
var arena_instance = fuzz_init.arena_state.*.promote(sample_child_allocator);
|
|
defer fuzz_init.arena_state.* = arena_instance.state;
|
|
|
|
var ctx: FuzzContext = .init(
|
|
control_allocator,
|
|
arena_instance.allocator(),
|
|
);
|
|
defer ctx.deinit();
|
|
|
|
var group: std.Io.Group = .init;
|
|
defer group.cancel(io);
|
|
|
|
var n_allocs: usize = 0;
|
|
var n_actions: usize = 0;
|
|
while (!smith.eosWeightedSimple(99, 1) and n_actions < FuzzContext.max_action_count) {
|
|
errdefer comptime unreachable;
|
|
|
|
const weights: []const testing.Smith.Weight = if (n_allocs == FuzzContext.max_alloc_count)
|
|
&.{
|
|
.value(FuzzContext.Action, .resize, 1),
|
|
.value(FuzzContext.Action, .remap, 1),
|
|
.value(FuzzContext.Action, .free, 1),
|
|
}
|
|
else
|
|
&.{
|
|
.value(FuzzContext.Action, .resize, 1),
|
|
.value(FuzzContext.Action, .remap, 1),
|
|
.value(FuzzContext.Action, .free, 1),
|
|
.value(FuzzContext.Action, .alloc, 3),
|
|
};
|
|
switch (smith.valueWeighted(FuzzContext.Action, weights)) {
|
|
.alloc => {
|
|
const alloc_index = n_allocs;
|
|
n_allocs += 1;
|
|
ctx.allocs[alloc_index].common.len = .free;
|
|
group.concurrent(io, FuzzContext.doOneAlloc, .{
|
|
&ctx,
|
|
nextLen(smith),
|
|
smith.valueRangeAtMost(
|
|
Alignment,
|
|
.@"1",
|
|
.fromByteUnits(2 * std.heap.page_size_max),
|
|
),
|
|
@enumFromInt(alloc_index),
|
|
}) catch unreachable;
|
|
},
|
|
.resize => group.concurrent(io, FuzzContext.doOneResize, .{ &ctx, nextLen(smith) }) catch unreachable,
|
|
.remap => group.concurrent(io, FuzzContext.doOneRemap, .{ &ctx, nextLen(smith) }) catch unreachable,
|
|
.free => group.concurrent(io, FuzzContext.doOneFree, .{&ctx}) catch unreachable,
|
|
}
|
|
n_actions += 1;
|
|
}
|
|
|
|
try group.await(io);
|
|
try ctx.check(n_allocs);
|
|
|
|
// This also covers the `deinit` logic since `free_all` uses it internally.
|
|
|
|
const old_capacity = arena_instance.queryCapacity();
|
|
const reset_mode: ResetMode = switch (smith.value(@typeInfo(ResetMode).@"union".tag_type.?)) {
|
|
.free_all => .free_all,
|
|
.retain_capacity => .retain_capacity,
|
|
.retain_with_limit => .{ .retain_with_limit = smith.value(usize) },
|
|
};
|
|
const ok = arena_instance.reset(reset_mode);
|
|
const new_capacity = arena_instance.queryCapacity();
|
|
switch (reset_mode) {
|
|
.free_all => {
|
|
try testing.expect(ok);
|
|
try testing.expectEqual(0, new_capacity);
|
|
fuzz_init.sample_instance.reset();
|
|
},
|
|
.retain_with_limit => |limit| if (ok) try testing.expect(new_capacity <= limit),
|
|
.retain_capacity => if (ok) try testing.expectEqual(old_capacity, new_capacity),
|
|
}
|
|
|
|
fuzz_init.control_instance.reset();
|
|
}
|
|
fn nextLen(smith: *std.testing.Smith) @typeInfo(FuzzContext.Alloc.Len).@"enum".tag_type {
|
|
@disableInstrumentation();
|
|
const BackingInt = @typeInfo(FuzzContext.Alloc.Len).@"enum".tag_type;
|
|
return smith.valueRangeAtMost(BackingInt, 1, FuzzContext.max_alloc_size);
|
|
}
|
|
|
|
const FuzzContext = struct {
|
|
control_allocator: Allocator,
|
|
sample_allocator: Allocator,
|
|
|
|
last_alloc_index: Alloc.Index,
|
|
allocs: [max_alloc_count]Alloc,
|
|
|
|
const max_alloc_count = 64;
|
|
const max_action_count = 2 * max_alloc_count;
|
|
|
|
const max_alloc_size = 16 << 10;
|
|
|
|
const Alloc = struct {
|
|
control_ptr: [*]u8,
|
|
sample_ptr: [*]u8,
|
|
common: packed struct(usize) {
|
|
len: Len,
|
|
alignment: Alignment,
|
|
_: @Int(.unsigned, padding_bits) = 0,
|
|
},
|
|
|
|
const Len = enum(@Int(.unsigned, len_bits)) {
|
|
free = (1 << len_bits) - 1,
|
|
_,
|
|
};
|
|
const len_bits = @min(64, @bitSizeOf(usize)) - @bitSizeOf(Alignment);
|
|
const padding_bits = @bitSizeOf(usize) - (len_bits + @bitSizeOf(Alignment));
|
|
|
|
const Index = enum(usize) {
|
|
none = std.math.maxInt(usize),
|
|
_,
|
|
};
|
|
};
|
|
|
|
const Action = enum {
|
|
alloc,
|
|
resize,
|
|
remap,
|
|
free,
|
|
};
|
|
|
|
const Init = struct {
|
|
threaded_instance: *std.Io.Threaded,
|
|
arena_state: *ArenaAllocator.State,
|
|
control_instance: *std.heap.FixedBufferAllocator,
|
|
sample_instance: *FuzzAllocator,
|
|
};
|
|
|
|
fn init(
|
|
control_allocator: Allocator,
|
|
sample_allocator: Allocator,
|
|
) FuzzContext {
|
|
@disableInstrumentation();
|
|
return .{
|
|
.control_allocator = control_allocator,
|
|
.sample_allocator = sample_allocator,
|
|
.last_alloc_index = .none,
|
|
.allocs = undefined,
|
|
};
|
|
}
|
|
|
|
fn deinit(ctx: *FuzzContext) void {
|
|
@disableInstrumentation();
|
|
ctx.* = undefined;
|
|
}
|
|
|
|
fn check(ctx: *const FuzzContext, n_allocs: usize) !void {
|
|
@disableInstrumentation();
|
|
for (ctx.allocs[0..n_allocs]) |allocation| {
|
|
const len: usize = switch (allocation.common.len) {
|
|
.free => continue,
|
|
_ => |len| @intFromEnum(len),
|
|
};
|
|
const control = allocation.control_ptr[0..len];
|
|
const sample = allocation.sample_ptr[0..len];
|
|
try std.testing.expectEqualSlices(u8, control, sample);
|
|
}
|
|
}
|
|
|
|
fn doOneAlloc(ctx: *FuzzContext, len: usize, alignment: Alignment, index: Alloc.Index) void {
|
|
@disableInstrumentation();
|
|
assert(ctx.allocs[@intFromEnum(index)].common.len == .free);
|
|
|
|
const control_ptr = ctx.control_allocator.rawAlloc(len, alignment, @returnAddress()) orelse
|
|
return;
|
|
const sample_ptr = ctx.sample_allocator.rawAlloc(len, alignment, @returnAddress()) orelse {
|
|
ctx.control_allocator.rawFree(control_ptr[0..len], alignment, @returnAddress());
|
|
return;
|
|
};
|
|
|
|
ctx.allocs[@intFromEnum(index)] = .{
|
|
.control_ptr = control_ptr,
|
|
.sample_ptr = sample_ptr,
|
|
.common = .{
|
|
.len = @enumFromInt(len),
|
|
.alignment = alignment,
|
|
},
|
|
};
|
|
|
|
for (control_ptr[0..len], sample_ptr[0..len], 0..) |*control, *sample, i| {
|
|
control.* = @truncate(i);
|
|
sample.* = @truncate(i);
|
|
}
|
|
|
|
@atomicStore(Alloc.Index, &ctx.last_alloc_index, index, .release);
|
|
}
|
|
fn doOneResize(ctx: *FuzzContext, new_len: usize) void {
|
|
@disableInstrumentation();
|
|
|
|
const index = @atomicRmw(Alloc.Index, &ctx.last_alloc_index, .Xchg, .none, .acquire);
|
|
if (index == .none) return;
|
|
|
|
const allocation = &ctx.allocs[@intFromEnum(index)];
|
|
assert(allocation.common.len != .free);
|
|
const memory = allocation.sample_ptr[0..@intFromEnum(allocation.common.len)];
|
|
const alignment = allocation.common.alignment;
|
|
|
|
assert(alignment.check(@intFromPtr(allocation.control_ptr)));
|
|
assert(alignment.check(@intFromPtr(allocation.sample_ptr)));
|
|
|
|
// Since `resize` is fallible, we have to ensure that `control_allocator`
|
|
// is always successful by reserving the memory we need beforehand.
|
|
const new_control_ptr = ctx.control_allocator.rawAlloc(new_len, alignment, @returnAddress()) orelse
|
|
return;
|
|
if (ctx.sample_allocator.rawResize(memory, alignment, new_len, @returnAddress())) {
|
|
const old_control = allocation.control_ptr[0..memory.len];
|
|
const overlap = @min(memory.len, new_len);
|
|
@memcpy(new_control_ptr[0..overlap], old_control[0..overlap]);
|
|
ctx.control_allocator.rawFree(old_control, alignment, @returnAddress());
|
|
} else {
|
|
ctx.control_allocator.rawFree(new_control_ptr[0..new_len], alignment, @returnAddress());
|
|
return;
|
|
}
|
|
|
|
ctx.allocs[@intFromEnum(index)] = .{
|
|
.control_ptr = new_control_ptr,
|
|
.sample_ptr = memory.ptr,
|
|
.common = .{
|
|
.len = @enumFromInt(new_len),
|
|
.alignment = alignment,
|
|
},
|
|
};
|
|
|
|
if (new_len > memory.len) {
|
|
for (
|
|
allocation.control_ptr[memory.len..new_len],
|
|
allocation.sample_ptr[memory.len..new_len],
|
|
0..,
|
|
) |*control, *sample, i| {
|
|
control.* = @truncate(i);
|
|
sample.* = @truncate(i);
|
|
}
|
|
}
|
|
|
|
@atomicStore(Alloc.Index, &ctx.last_alloc_index, index, .release);
|
|
}
|
|
fn doOneRemap(ctx: *FuzzContext, new_len: usize) void {
|
|
@disableInstrumentation();
|
|
return doOneResize(ctx, new_len);
|
|
}
|
|
fn doOneFree(ctx: *FuzzContext) void {
|
|
@disableInstrumentation();
|
|
|
|
const index = @atomicRmw(Alloc.Index, &ctx.last_alloc_index, .Xchg, .none, .acquire);
|
|
if (index == .none) return;
|
|
|
|
const allocation = &ctx.allocs[@intFromEnum(index)];
|
|
assert(allocation.common.len != .free);
|
|
const len: usize = @intFromEnum(allocation.common.len);
|
|
const alignment = allocation.common.alignment;
|
|
|
|
assert(alignment.check(@intFromPtr(allocation.control_ptr)));
|
|
assert(alignment.check(@intFromPtr(allocation.sample_ptr)));
|
|
|
|
ctx.control_allocator.rawFree(allocation.control_ptr[0..len], alignment, @returnAddress());
|
|
ctx.sample_allocator.rawFree(allocation.sample_ptr[0..len], alignment, @returnAddress());
|
|
|
|
ctx.allocs[@intFromEnum(index)] = .{
|
|
.control_ptr = undefined,
|
|
.sample_ptr = undefined,
|
|
.common = .{
|
|
.len = .free,
|
|
.alignment = .@"1",
|
|
},
|
|
};
|
|
}
|
|
};
|
|
|
|
const FuzzAllocator = struct {
|
|
fba: std.heap.FixedBufferAllocator,
|
|
spurious_failures: [256]u8,
|
|
index: u8,
|
|
|
|
fn init(buffer: []u8) FuzzAllocator {
|
|
@disableInstrumentation();
|
|
return .{
|
|
.fba = .init(buffer),
|
|
.spurious_failures = undefined, // set with `preprepareFailures`
|
|
.index = 0,
|
|
};
|
|
}
|
|
|
|
fn prepareFailures(fa: *FuzzAllocator, smith: *std.testing.Smith) void {
|
|
@disableInstrumentation();
|
|
const bool_weights: []const std.testing.Smith.Weight = &.{
|
|
.value(u8, 0, 10),
|
|
.value(u8, 1, 1),
|
|
};
|
|
smith.bytesWeighted(&fa.spurious_failures, bool_weights);
|
|
fa.index = 0;
|
|
}
|
|
|
|
fn reset(fa: *FuzzAllocator) void {
|
|
@disableInstrumentation();
|
|
fa.fba.reset();
|
|
}
|
|
|
|
fn allocator(fa: *FuzzAllocator) Allocator {
|
|
@disableInstrumentation();
|
|
return .{
|
|
.ptr = fa,
|
|
.vtable = &.{
|
|
.alloc = FuzzAllocator.alloc,
|
|
.resize = FuzzAllocator.resize,
|
|
.remap = FuzzAllocator.remap,
|
|
.free = FuzzAllocator.free,
|
|
},
|
|
};
|
|
}
|
|
|
|
fn alloc(ctx: *anyopaque, len: usize, alignment: Alignment, ret_addr: usize) ?[*]u8 {
|
|
@disableInstrumentation();
|
|
const fa: *FuzzAllocator = @ptrCast(@alignCast(ctx));
|
|
_ = ret_addr;
|
|
|
|
const index = @atomicRmw(u8, &fa.index, .Add, 1, .monotonic);
|
|
if (fa.spurious_failures[index] != 0) return null;
|
|
return fa.fba.threadSafeAllocator().rawAlloc(len, alignment, @returnAddress());
|
|
}
|
|
|
|
fn resize(ctx: *anyopaque, memory: []u8, alignment: Alignment, new_len: usize, ret_addr: usize) bool {
|
|
@disableInstrumentation();
|
|
const fa: *FuzzAllocator = @ptrCast(@alignCast(ctx));
|
|
_ = ret_addr;
|
|
|
|
const index = @atomicRmw(u8, &fa.index, .Add, 1, .monotonic);
|
|
if (fa.spurious_failures[index] != 0) return false;
|
|
return fa.fba.threadSafeAllocator().rawResize(memory, alignment, new_len, @returnAddress());
|
|
}
|
|
|
|
fn remap(ctx: *anyopaque, memory: []u8, alignment: Alignment, new_len: usize, ret_addr: usize) ?[*]u8 {
|
|
@disableInstrumentation();
|
|
const fa: *FuzzAllocator = @ptrCast(@alignCast(ctx));
|
|
_ = ret_addr;
|
|
|
|
const index = @atomicRmw(u8, &fa.index, .Add, 1, .monotonic);
|
|
if (fa.spurious_failures[index] != 0) return null;
|
|
return fa.fba.threadSafeAllocator().rawRemap(memory, alignment, new_len, @returnAddress());
|
|
}
|
|
|
|
fn free(ctx: *anyopaque, memory: []u8, alignment: Alignment, ret_addr: usize) void {
|
|
@disableInstrumentation();
|
|
const fa: *FuzzAllocator = @ptrCast(@alignCast(ctx));
|
|
_ = ret_addr;
|
|
return fa.fba.threadSafeAllocator().rawFree(memory, alignment, @returnAddress());
|
|
}
|
|
};
|