Backlog/lib/p2/src/structures/paged-vector.zig

287 lines
9.3 KiB
Zig

const std = @import("std");
// it's like an normal vector/ arraylist except it never invalidates old pointers.
// and never causes re-allocations. (excepting in the control block, which is just a vector of things)
//
// It is implemented with a control block which which contains a list of pages.
// these pages contain the actual memory of the vector.
//
// it has a growth factor, which can be configured.
//
// use cases:
//
// - this is highly performant for cases where
// - YOU WANT generally fast insertion time
// - YOU WANT fast random access time
// - YOU WANT cache-coherent and efficient linear iteration
// - YOU WANT to retain pointers across operations
// - YOU WANT to avoid reallocations (eg. if the data set is remarkably big)
// - YOU KNOW that you have linear growth factors and exponential growth is overkill.
//
// great for:
// - event logging.
// - output buffering.
// - recording timelines.
pub fn PagedVectorAdvanced(comptime T: type, comptime growth: usize) type {
return struct {
const Page = struct {
data: [growth]T align(8) = undefined,
len: u32 = 0,
};
pages: std.ArrayListUnmanaged(*Page) = .{},
currentPageId: usize = 0,
head: *T,
pub fn init(allocator: std.mem.Allocator) !@This() {
var rv = @This(){
.head = undefined,
};
const page = try allocator.create(Page);
page.* = .{};
try rv.pages.append(allocator, page);
rv.head = &rv.pages.items[0].data[0];
return rv;
}
pub fn capacity(self: @This()) usize {
return self.pages.items.len * growth;
}
pub fn len(self: @This()) usize {
return (self.currentPageId) * growth + self.currentPage().len;
}
pub fn deinit(self: *@This(), allocator: std.mem.Allocator) void {
for (self.pages.items) |page| {
allocator.destroy(page);
}
self.pages.deinit(allocator);
}
fn maybeExpand(self: *@This(), allocator: std.mem.Allocator) !void {
if (self.currentPageId == self.pages.items.len - 1) {
const page = try allocator.create(Page);
page.* = .{};
try self.pages.append(allocator, page);
self.currentPageId = self.pages.items.len - 1;
} else {
self.currentPageId += 1;
}
}
// frees all pages. and resets back to default state
pub fn reset(self: *@This(), allocator: std.mem.Allocator) void {
if (self.pages.items.len > 1) {
for (self.pages.items[1..]) |page| {
allocator.destroy(page);
}
}
self.currentPageId = 0;
self.head = &self.pages.items[0].data[0];
}
pub fn append(self: *@This(), allocator: std.mem.Allocator, value: T) !void {
var page = self.currentPage();
page.len += 1;
if (page.len >= growth) {
try self.maybeExpand(allocator);
var appendedPage = self.pages.items[self.pages.items.len - 1];
self.head = &appendedPage.data[appendedPage.len];
} else {
self.head = &page.data[page.len - 1];
}
self.head.* = value;
}
pub fn appendSlice(self: *@This(), allocator: std.mem.Allocator, slice: []const T) !void {
for (slice) |value| {
try self.append(allocator, value);
}
}
pub fn getMutable(self: *@This(), index: usize) *T {
const pageIndex = std.math.divFloor(usize, index, growth) catch unreachable;
return &self.pages.items[pageIndex].data[index % growth];
}
pub fn get(self: @This(), index: usize) *const T {
const pageIndex = std.math.divFloor(usize, index, growth) catch unreachable;
return &self.pages.items[pageIndex].data[index % growth];
}
// swaps the element at the index with the element currently pointed to by head
// then pops back the head, shrinking length by 1
//
// Efficient removal of an element at an index but does not preserve order.
pub fn swapRemove(self: *@This(), index: usize) void {
const ptr = self.getMutable(index);
ptr.* = self.head.*;
_ = try self.pop();
}
fn currentPage(self: @This()) *Page {
return self.pages.items[self.currentPageId];
}
// // retrieves a value, shrinking length by 1
pub fn pop(self: *@This()) !T {
const rv = self.head.*;
var page = self.currentPage();
page.len -= 1;
if (page.len < 1) {
page.len = 0;
self.currentPageId -= 1;
}
self.head = &self.currentPage().data[self.currentPage().len - 1];
return rv;
}
};
}
pub fn PagedVectorUnmanaged(comptime T: type) type {
return PagedVectorAdvanced(T, 1024);
}
pub fn PagedVector(comptime T: type) type {
return struct {
const VectorType = PagedVectorAdvanced(T, 1024);
vector: VectorType,
allocator: std.mem.Allocator,
pub fn init(allocator: std.mem.Allocator) !@This() {
return .{
.vector = try VectorType.init(allocator),
.allocator = allocator,
};
}
pub fn deinit(self: *@This()) void {
self.vector.deinit(self.allocator);
}
pub fn appendSlice(self: *@This(), slice: []const T) !void {
try self.vector.appendSlice(self.allocator, slice);
}
pub fn append(self: *@This(), value: T) !void {
try self.vector.append(self.allocator, value);
}
pub fn appendAndGet(self: *@This(), value: T) !*T {
const index = self.len();
try self.append(value);
return self.getMutable(index);
}
pub fn get(self: *const @This(), index: usize) *const T {
return self.vector.get(index);
}
pub fn getMutable(self: *@This(), index: usize) *T {
return self.vector.getMutable(index);
}
pub fn len(self: @This()) usize {
return self.vector.len();
}
pub fn capacity(self: @This()) usize {
return self.vector.capacity();
}
pub fn swapRemove(self: *@This(), index: usize) void {
self.vector.swapRemove(index);
}
pub fn reset(self: *@This()) void {
self.vector.reset(self.allocator);
}
// // retrieves a value, shrinking length by 1
pub fn pop(self: *@This()) !T {
return try self.vector.pop();
}
};
}
test "simple loading multiple pages." {
const TestStruct = struct {
lmao: usize,
};
const allocator = std.testing.allocator;
var vector = try PagedVectorAdvanced(TestStruct, 1024).init(std.testing.allocator);
defer vector.deinit(allocator);
for (0..128) |j| {
try vector.append(allocator, .{ .lmao = j });
}
const ptr = vector.get(0);
const mutable = vector.getMutable(127);
for (0..4096) |i| {
try vector.append(allocator, .{ .lmao = i });
}
// testing that pointers are not invalidated
std.debug.assert(ptr == vector.get(0));
std.debug.assert(mutable == vector.getMutable(127));
std.debug.assert(vector.getMutable(4092) == vector.getMutable(4092));
// 4096 + 128 = 4224 = 4 pages + 128 extra entries
std.debug.assert(vector.pages.items.len == 5);
std.debug.assert(vector.capacity() == 1024 * 5);
std.debug.assert(vector.len() == 128 + 1024 * 4);
// 4092 - 3072 = offset of 1020 in the third page.
const ptr4092 = vector.getMutable(4092);
std.debug.assert(vector.getMutable(4092) == &vector.pages.items[3].data[1020]);
// lets try adding 1024 elements and see if pointers are still preserved.
for (0..1024) |_| {
try vector.append(allocator, .{ .lmao = 0 });
}
std.debug.assert(vector.getMutable(4092) == ptr4092);
std.debug.assert(vector.get(0).lmao == 0);
std.debug.assert(vector.get(1).lmao == 1);
std.debug.assert(vector.get(2).lmao == 2);
// testing the pop() function
std.debug.assert(vector.len() == 128 + 1024 * 5);
_ = try vector.pop();
_ = try vector.pop();
_ = try vector.pop();
// testing the swapRemove Functionality
std.debug.assert(vector.len() == 128 + 1024 * 5 - 3);
vector.swapRemove(1);
std.debug.assert(vector.len() == 128 + 1024 * 5 - 4);
for (0..1024) |_| {
vector.swapRemove(0);
}
std.debug.assert(vector.len() == 128 + 1024 * 5 - 4 - 1024);
}
test "managed version" {
const allocator = std.testing.allocator;
var vec = try PagedVector(struct { lmao: u64 }).init(allocator);
defer vec.deinit();
for (0..8192) |i| {
try vec.append(.{ .lmao = i });
}
std.debug.assert(vec.vector.pages.items.len == 9);
}