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); }