This commit is contained in:
2025-11-20 08:58:45 +01:00
commit 27f985bedf
13 changed files with 68972 additions and 0 deletions

424
src/AddressAllocator.zig Normal file
View File

@@ -0,0 +1,424 @@
const std = @import("std");
const mem = std.mem;
const sort = std.sort;
const testing = std.testing;
const assert = std.debug.assert;
const Range = @import("Range.zig");
const log = std.log.scoped(.address_allocator);
const AddressAllocator = @This();
/// The **sorted** list of `Range`s that are blocked.
ranges: std.ArrayListUnmanaged(Range) = .empty,
pub const empty = AddressAllocator{};
pub fn deinit(address_allocator: *AddressAllocator, gpa: mem.Allocator) void {
address_allocator.ranges.deinit(gpa);
}
/// Block a range to not be used by the `allocate` function. This function will always succeed, if
/// there is enough memory available.
pub fn block(
address_allocator: *AddressAllocator,
gpa: mem.Allocator,
range: Range,
alignment: u64,
) !void {
assert(address_allocator.isSorted());
defer assert(address_allocator.isSorted());
const aligned_range = if (alignment != 0) range.alignTo(alignment) else range;
assert(aligned_range.contains(range));
if (aligned_range.size() == 0) return;
// Find the correct sorted position to insert the new range.
const insert_idx = sort.lowerBound(
Range,
address_allocator.ranges.items,
aligned_range,
Range.compare,
);
log.debug(
"block: range: {}, alignment: {}, aligned_range: {}, insert_idx: {}",
.{ range, alignment, aligned_range, insert_idx },
);
// If the new range is the greatest one OR if the entry at `insert_idx` is greater than the
// new range, we can just insert.
if (insert_idx == address_allocator.ranges.items.len or
address_allocator.ranges.items[insert_idx].compare(aligned_range) == .gt)
{
log.debug("block: New range inserted", .{});
return address_allocator.ranges.insert(gpa, insert_idx, aligned_range);
}
errdefer comptime unreachable;
assert(address_allocator.ranges.items.len > 0);
// Now `insert_idx` points to the first entry, that touches `aligned_range`.
assert(address_allocator.ranges.items[insert_idx].touches(aligned_range));
if (insert_idx > 1 and address_allocator.ranges.items.len > 1) {
assert(!address_allocator.ranges.items[insert_idx - 1].touches(aligned_range));
}
log.debug("block: `aligned_range` touches at least one existing range.", .{});
// NOTE: We merge entries that touch eachother to speedup future traversals.
// There are a few cases how to handle the merging:
// 1. `aligned_range` is contained by the existing range. Then we have to do nothing and can
// return early.
// 2. `aligned_range` contains the existing range. Then we have to overwrite `start` and `end`.
// 3. The existing range is before `aligned_range`. Set `existing.end` to `aligned_range.end`.
// 4. The existing range is after `aligned_range`. Set `existing.start` to `aligned.start`.
// After we have done this to the first range that touches, we will loop over the other ones
// that touch and just have to apply rule 4 repeatedly.
const first = &address_allocator.ranges.items[insert_idx];
if (first.contains(aligned_range)) {
log.debug("block: Existing range at index {} contains new range. No-op", .{insert_idx});
return;
} else if (aligned_range.contains(first.*)) {
log.debug(
"block: New range contains existing range at index {}: {} -> {}",
.{ insert_idx, first, aligned_range },
);
first.* = aligned_range;
} else if (aligned_range.start <= first.end and aligned_range.end >= first.end) {
assert(aligned_range.start > first.start);
log.debug(
"block: Adjusting range end at index {}: {} -> {}",
.{ insert_idx, first.end, aligned_range.end },
);
first.*.end = aligned_range.end;
} else if (aligned_range.end >= first.start and aligned_range.start <= first.start) {
assert(aligned_range.end < first.end);
log.debug(
"block: Adjusting range start at index {}: {} -> {}",
.{ insert_idx, first.start, aligned_range.start },
);
first.*.start = aligned_range.start;
} else {
unreachable;
}
// TODO: comment why we do this
if (insert_idx >= address_allocator.ranges.items.len - 1) return;
var neighbor = &address_allocator.ranges.items[insert_idx + 1];
var i: u64 = 0;
while (neighbor.touches(aligned_range)) {
assert(aligned_range.end >= neighbor.start);
assert(aligned_range.start <= neighbor.start);
if (neighbor.end > first.end) {
log.debug(
"block: Merging neighbor range at index {}: {} -> {}.",
.{ insert_idx + 1, first.end, neighbor.end },
);
first.end = neighbor.end;
}
const removed = address_allocator.ranges.orderedRemove(insert_idx + 1);
log.debug("block: Removed merged range: {}", .{removed});
i += 1;
}
log.debug("block: Removed {} ranges.", .{i});
}
/// Allocate and block a `Range` of size `size` which will lie inside the given `valid_range`. If no
/// allocation of the given size is possible, return `null`.
pub fn allocate(
address_allocator: *AddressAllocator,
gpa: mem.Allocator,
size: u64,
valid_range: Range,
) !?Range {
log.debug("allocate: Allocating size {} in range {}", .{ size, valid_range });
if (valid_range.size() < size) return null;
if (size == 0) return null;
const size_i: i64 = @intCast(size);
// OPTIM: Use binary search to find the start of the valid range inside the reserved ranges.
// `candidate_start` tracks the beginning of the current free region being examined.
var candidate_start = valid_range.start;
for (address_allocator.ranges.items) |reserved| {
if (candidate_start >= valid_range.end) {
log.debug("allocate: Searched past the valid range.", .{});
break;
}
// The potential allocation gap is before the current reserved block.
if (candidate_start < reserved.start) {
// Determine the actual available portion of the gap within our search `range`.
const gap_end = @min(reserved.start, valid_range.end);
if (gap_end >= candidate_start + size_i) {
const new_range = Range{
.start = candidate_start,
.end = candidate_start + size_i,
};
try address_allocator.block(gpa, new_range, 0);
assert(valid_range.contains(new_range));
log.debug("allocate: Found free gap: {}", .{new_range});
return new_range;
}
}
// The gap was not large enough. Move the candidate start past the current reserved block
// for the next iteration.
candidate_start = @max(candidate_start, reserved.end);
}
// Check the remaining space at the end of the search range.
if (valid_range.end >= candidate_start + size_i) {
const new_range = Range{
.start = candidate_start,
.end = candidate_start + size_i,
};
try address_allocator.block(gpa, new_range, 0);
assert(valid_range.contains(new_range));
log.debug("allocate: Found free gap at end: {}", .{new_range});
return new_range;
}
log.debug("allocate: No suitable gap found.", .{});
return null;
}
fn isSorted(address_allocator: *const AddressAllocator) bool {
return sort.isSorted(Range, address_allocator.ranges.items, {}, isSortedInner);
}
fn isSortedInner(_: void, lhs: Range, rhs: Range) bool {
return switch (lhs.compare(rhs)) {
.lt => true,
.gt => false,
.eq => unreachable,
};
}
test "block basic" {
var aa = AddressAllocator{};
defer aa.deinit(testing.allocator);
try aa.block(testing.allocator, .{ .start = 0, .end = 100 }, 0);
try testing.expectEqual(Range{ .start = 0, .end = 100 }, aa.ranges.items[0]);
try aa.block(testing.allocator, .{ .start = 200, .end = 300 }, 0);
try testing.expectEqual(Range{ .start = 0, .end = 100 }, aa.ranges.items[0]);
try testing.expectEqual(Range{ .start = 200, .end = 300 }, aa.ranges.items[1]);
try testing.expectEqual(2, aa.ranges.items.len);
}
test "block in hole" {
var aa = AddressAllocator{};
defer aa.deinit(testing.allocator);
try aa.block(testing.allocator, .{ .start = 0, .end = 100 }, 0);
try testing.expectEqual(Range{ .start = 0, .end = 100 }, aa.ranges.items[0]);
try aa.block(testing.allocator, .{ .start = 400, .end = 500 }, 0);
try testing.expectEqual(2, aa.ranges.items.len);
try testing.expectEqual(Range{ .start = 0, .end = 100 }, aa.ranges.items[0]);
try testing.expectEqual(Range{ .start = 400, .end = 500 }, aa.ranges.items[1]);
try aa.block(testing.allocator, .{ .start = 200, .end = 300 }, 0);
try testing.expectEqual(3, aa.ranges.items.len);
try testing.expectEqual(Range{ .start = 0, .end = 100 }, aa.ranges.items[0]);
try testing.expectEqual(Range{ .start = 200, .end = 300 }, aa.ranges.items[1]);
try testing.expectEqual(Range{ .start = 400, .end = 500 }, aa.ranges.items[2]);
}
test "block touch with previous" {
var aa = AddressAllocator{};
defer aa.deinit(testing.allocator);
try aa.block(testing.allocator, .{ .start = 0, .end = 100 }, 0);
try aa.block(testing.allocator, .{ .start = 100, .end = 200 }, 0);
try testing.expectEqual(Range{ .start = 0, .end = 200 }, aa.ranges.items[0]);
try testing.expectEqual(1, aa.ranges.items.len);
try aa.block(testing.allocator, .{ .start = 100, .end = 300 }, 0);
try testing.expectEqual(Range{ .start = 0, .end = 300 }, aa.ranges.items[0]);
try testing.expectEqual(1, aa.ranges.items.len);
try aa.block(testing.allocator, .{ .start = 300, .end = 400 }, 0);
try testing.expectEqual(Range{ .start = 0, .end = 400 }, aa.ranges.items[0]);
try testing.expectEqual(1, aa.ranges.items.len);
}
test "block touch with following" {
var aa = AddressAllocator{};
defer aa.deinit(testing.allocator);
try aa.block(testing.allocator, .{ .start = 200, .end = 300 }, 0);
try aa.block(testing.allocator, .{ .start = 100, .end = 200 }, 0);
try testing.expectEqual(Range{ .start = 100, .end = 300 }, aa.ranges.items[0]);
try testing.expectEqual(1, aa.ranges.items.len);
try aa.block(testing.allocator, .{ .start = 0, .end = 200 }, 0);
try testing.expectEqual(Range{ .start = 0, .end = 300 }, aa.ranges.items[0]);
try testing.expectEqual(1, aa.ranges.items.len);
try aa.block(testing.allocator, .{ .start = -100, .end = 0 }, 0);
try testing.expectEqual(Range{ .start = -100, .end = 300 }, aa.ranges.items[0]);
try testing.expectEqual(1, aa.ranges.items.len);
}
test "block overlap with previous and following" {
var aa = AddressAllocator{};
defer aa.deinit(testing.allocator);
try aa.block(testing.allocator, .{ .start = 0, .end = 100 }, 0);
try aa.block(testing.allocator, .{ .start = 200, .end = 300 }, 0);
try testing.expectEqual(Range{ .start = 0, .end = 100 }, aa.ranges.items[0]);
try testing.expectEqual(Range{ .start = 200, .end = 300 }, aa.ranges.items[1]);
try testing.expectEqual(2, aa.ranges.items.len);
try aa.block(testing.allocator, .{ .start = 50, .end = 250 }, 0);
try testing.expectEqual(Range{ .start = 0, .end = 300 }, aa.ranges.items[0]);
try testing.expectEqual(1, aa.ranges.items.len);
}
test "block contained by existing" {
var aa = AddressAllocator{};
defer aa.deinit(testing.allocator);
try aa.block(testing.allocator, .{ .start = 100, .end = 300 }, 0);
try aa.block(testing.allocator, .{ .start = 200, .end = 250 }, 0);
try testing.expectEqual(Range{ .start = 100, .end = 300 }, aa.ranges.items[0]);
try testing.expectEqual(1, aa.ranges.items.len);
}
test "block contains existing" {
var aa = AddressAllocator{};
defer aa.deinit(testing.allocator);
try aa.block(testing.allocator, .{ .start = 50, .end = 100 }, 0);
try aa.block(testing.allocator, .{ .start = 0, .end = 200 }, 0);
try testing.expectEqual(Range{ .start = 0, .end = 200 }, aa.ranges.items[0]);
try testing.expectEqual(1, aa.ranges.items.len);
}
test "block overlaps multiple" {
var aa = AddressAllocator{};
defer aa.deinit(testing.allocator);
try aa.block(testing.allocator, .{ .start = 0, .end = 100 }, 0);
try aa.block(testing.allocator, .{ .start = 150, .end = 200 }, 0);
try aa.block(testing.allocator, .{ .start = 250, .end = 300 }, 0);
try aa.block(testing.allocator, .{ .start = 350, .end = 400 }, 0);
try aa.block(testing.allocator, .{ .start = 450, .end = 500 }, 0);
try testing.expectEqual(5, aa.ranges.items.len);
try aa.block(testing.allocator, .{ .start = 50, .end = 475 }, 0);
try testing.expectEqual(Range{ .start = 0, .end = 500 }, aa.ranges.items[0]);
try testing.expectEqual(1, aa.ranges.items.len);
}
test "allocate in empty allocator" {
var aa = AddressAllocator{};
defer aa.deinit(testing.allocator);
const search_range = Range{ .start = 0, .end = 1000 };
const allocated = try aa.allocate(testing.allocator, 100, search_range);
try testing.expectEqual(1, aa.ranges.items.len);
try testing.expectEqual(Range{ .start = 0, .end = 100 }, aa.ranges.items[0]);
try testing.expectEqual(Range{ .start = 0, .end = 100 }, allocated);
}
test "allocate with no space" {
var aa = AddressAllocator{};
defer aa.deinit(testing.allocator);
const range = Range{ .start = 0, .end = 1000 };
try aa.block(testing.allocator, range, 0);
const allocated = try aa.allocate(testing.allocator, 100, range);
try testing.expect(allocated == null);
}
test "allocate in a gap" {
var aa = AddressAllocator{};
defer aa.deinit(testing.allocator);
try aa.block(testing.allocator, .{ .start = 0, .end = 100 }, 0);
try aa.block(testing.allocator, .{ .start = 200, .end = 300 }, 0);
const search_range = Range{ .start = 0, .end = 1000 };
const allocated = try aa.allocate(testing.allocator, 50, search_range);
try testing.expectEqual(Range{ .start = 100, .end = 150 }, allocated);
try testing.expectEqual(2, aa.ranges.items.len);
try testing.expectEqual(Range{ .start = 0, .end = 150 }, aa.ranges.items[0]);
try testing.expectEqual(Range{ .start = 200, .end = 300 }, aa.ranges.items[1]);
}
test "allocate at the end" {
var aa = AddressAllocator{};
defer aa.deinit(testing.allocator);
try aa.block(testing.allocator, .{ .start = 0, .end = 100 }, 0);
const search_range = Range{ .start = 0, .end = 1000 };
const allocated = try aa.allocate(testing.allocator, 200, search_range);
try testing.expectEqual(Range{ .start = 100, .end = 300 }, allocated);
try testing.expectEqual(1, aa.ranges.items.len);
try testing.expectEqual(Range{ .start = 0, .end = 300 }, aa.ranges.items[0]);
}
test "allocate within specific search range" {
var aa = AddressAllocator{};
defer aa.deinit(testing.allocator);
try aa.block(testing.allocator, .{ .start = 0, .end = 100 }, 0);
try aa.block(testing.allocator, .{ .start = 400, .end = 500 }, 0);
// Search range starts after first block and has a gap
const search_range = Range{ .start = 200, .end = 400 };
const allocated = try aa.allocate(testing.allocator, 100, search_range);
try testing.expectEqual(Range{ .start = 200, .end = 300 }, allocated);
try testing.expectEqual(3, aa.ranges.items.len);
try testing.expectEqual(Range{ .start = 0, .end = 100 }, aa.ranges.items[0]);
try testing.expectEqual(Range{ .start = 400, .end = 500 }, aa.ranges.items[2]);
try testing.expectEqual(Range{ .start = 200, .end = 300 }, aa.ranges.items[1]);
}
test "allocate exact gap size" {
var aa = AddressAllocator{};
defer aa.deinit(testing.allocator);
try aa.block(testing.allocator, .{ .start = 0, .end = 100 }, 0);
try aa.block(testing.allocator, .{ .start = 200, .end = 300 }, 0);
const search_range = Range{ .start = 0, .end = 1000 };
const allocated = try aa.allocate(testing.allocator, 100, search_range);
try testing.expectEqual(Range{ .start = 100, .end = 200 }, allocated);
try testing.expectEqual(1, aa.ranges.items.len);
try testing.expectEqual(Range{ .start = 0, .end = 300 }, aa.ranges.items[0]);
}
test "allocate fails when too large" {
var aa = AddressAllocator{};
defer aa.deinit(testing.allocator);
try aa.block(testing.allocator, .{ .start = 0, .end = 100 }, 0);
try aa.block(testing.allocator, .{ .start = 200, .end = 300 }, 0);
const search_range = Range{ .start = 0, .end = 400 };
const allocated = try aa.allocate(testing.allocator, 101, search_range);
try std.testing.expect(allocated == null);
}
test "allocate with zero size" {
var aa = AddressAllocator{};
defer aa.deinit(testing.allocator);
const search_range = Range{ .start = 0, .end = 1000 };
const allocated = try aa.allocate(testing.allocator, 0, search_range);
try std.testing.expect(allocated == null);
}
test "allocate with size bigger than range" {
var aa = AddressAllocator{};
defer aa.deinit(testing.allocator);
const search_range = Range{ .start = 0, .end = 100 };
const allocated = try aa.allocate(testing.allocator, 1000, search_range);
try std.testing.expect(allocated == null);
}