const std = @import("std"); // todo: // // - take() - done // - clone() - done // - resize() // // actually useful functions // - append() // - add() // - trim() // - substring() // - split() // - fmt() // // main purpose of this string-pool is to provide an RC-ableinterface for interface // with GC'ed systems such as lua var gStringContext: *StringContext = undefined; pub const String = struct { index: u24, bucketId: u8, bytes: ?[]u8 = null, // potentially slower, use utf8() whenever possible to cache the results pub fn getUtf8(self: @This()) []u8 { if (self.bytes) |bytes| { return bytes; } else { return gStringContext.getUtf8(self); } } // gets the utf8 value from this handle and caches the result if possible pub fn utf8(self: *@This()) []u8 { if (self.bytes) |bytes| { return bytes; } else { self.bytes = gStringContext.getUtf8(self.*); return self.bytes.?; } } // creates a new string from []const u8 pub fn new(str: []const u8) !@This() { return try gStringContext.newFromUtf8(str); } // deep copy of a string pub fn clone(self: @This()) !@This() { return try gStringContext.newFromUtf8(self.utf8()); } // increases the reference count for this string by one and makes a copy of the handle pub fn pin(self: @This()) @This() { gStringContext.rcAdd(self); return self; } pub fn drop(self: @This()) void { gStringContext.strDestroy(self); } }; // testing functions for string allocation const StringAllocation = struct { ptr: *align(2) anyopaque, pub const HeaderLen = 4; // offsets: // 0x0 -> 0x3 : length (u24) // 0x3 -> 0x4 : reference count (u8) // 0x4 -> length + 4 : string pub fn len(self: @This()) usize { const read = @as(*u24, @ptrCast(@alignCast(self.ptr))).*; return @intCast(read); } pub fn initRc(self: @This()) void { const x: [*]std.atomic.Value(u8) = @ptrCast(self.ptr); x[3] = std.atomic.Value(u8).init(1); } pub fn rcAdd(self: @This()) void { const x: [*]std.atomic.Value(u8) = @ptrCast(self.ptr); //+= 1; _ = x[3].fetchAdd(1, .monotonic); } // returns true if the reference count hits 0 pub fn rcSub(self: @This()) bool { const rc: [*]std.atomic.Value(u8) = @ptrCast(self.ptr); //+= 1; if (rc[3].fetchSub(1, .release) == 1) { return true; } return false; } pub fn setLen(self: @This(), length: usize) void { @as(*u24, @ptrCast(@alignCast(self.ptr))).* = @intCast(length); } pub fn bytes(self: @This()) []u8 { return @as([*]u8, @ptrCast(self.ptr))[HeaderLen .. HeaderLen + self.len()]; } // returns a bytes list for use with a memory allocator pub fn deallocBytes(self: @This()) []align(2) u8 { var rv: []align(2) u8 = undefined; rv.ptr = @ptrCast(self.ptr); rv.len = self.len() + StringAllocation.HeaderLen; return rv; } pub fn fromPtr(ptr: *anyopaque) @This() { return .{ .ptr = @ptrCast(@alignCast(ptr)) }; } }; const Page = struct { bytes: []align(8) u8, }; const Bucket = struct { id: u32, allocSize: u32, pageSize: u32, slotsPerPage: u32, next: u32 = 0, pages: std.ArrayListUnmanaged(Page) = .{}, freeSlots: std.ArrayListUnmanaged(u32) = .{}, lock: std.Thread.Mutex = .{}, pub const BucketAllocation = struct { bytes: []u8, index: u24, }; pub fn init(id: u32, allocSize: u32, pageSize: u32) @This() { return .{ .id = id, .allocSize = allocSize, .pageSize = pageSize, .slotsPerPage = pageSize / allocSize, }; } // index to page inline fn i2p(self: @This(), index: u32) u32 { return @divFloor(index, self.slotsPerPage); } // index based off of a page inline fn ibp(self: @This(), index: u32) u32 { return index % self.slotsPerPage; } fn getBytesFromIndex(self: @This(), index: u32) []u8 { return self.indexToSlot(self.i2p(index), self.ibp(index)); } fn addPage(self: *@This(), allocator: std.mem.Allocator) !void { const page: Page = .{ .bytes = try allocator.alignedAlloc(u8, .@"8", self.pageSize), }; try self.pages.append(allocator, page); } fn indexToSlot(self: @This(), pageIndex: u32, pageBase: u32) []u8 { const left = pageBase * self.allocSize; const right = left + self.allocSize; const slice = self.pages.items[pageIndex].bytes[left..right]; return slice; } pub fn destroySlot(self: *@This(), allocator: std.mem.Allocator, index: u32) void { self.lock.lock(); defer self.lock.unlock(); self.freeSlots.append(allocator, index) catch unreachable; } pub fn assignOrRecycleSlot(self: *@This(), allocator: std.mem.Allocator) !BucketAllocation { self.lock.lock(); defer self.lock.unlock(); if (self.freeSlots.items.len < 1) { const pageIndex = self.i2p(self.next); if (pageIndex >= self.pages.items.len) { try self.addPage(allocator); } const rv: BucketAllocation = .{ .bytes = self.indexToSlot(pageIndex, self.ibp(self.next)), .index = @intCast(self.next), }; self.next += 1; return rv; } else { const index = self.freeSlots.pop().?; const rv: BucketAllocation = .{ .bytes = self.indexToSlot(self.i2p(index), self.ibp(index)), .index = @intCast(index), }; return rv; } } }; pub const StringContext = struct { backingAllocator: std.mem.Allocator, arena: std.heap.ArenaAllocator, // for now, just do an arenaAllocator buckets: std.ArrayListUnmanaged(Bucket) = .{}, pub fn create(backingAllocator: std.mem.Allocator) !*@This() { const self = try backingAllocator.create(@This()); self.* = .{ .backingAllocator = backingAllocator, .arena = std.heap.ArenaAllocator.init(backingAllocator), }; // default bucket setup // zig fmt: off try self.buckets.appendSlice(backingAllocator, &.{ Bucket.init( 0x0, 16, 4096 * 1), Bucket.init( 0x1, 32, 4096 * 1), Bucket.init( 0x2, 64, 4096 * 1), Bucket.init( 0x3, 128, 4096 * 1), Bucket.init( 0x4, 512, 4096 * 16), Bucket.init( 0x5, 4096, 4096 * 16), }); // zig fmt: on return self; } pub fn getAllocator(self: *@This()) std.mem.Allocator { return self.arena.allocator(); } pub fn newFromUtf8(self: *@This(), str: []const u8) !String { const results = try self.strNew(str.len); std.mem.copyForwards(u8, results.bytes, str); return results.handle; } // inner function pub fn strNew(self: *@This(), length: usize) !struct { handle: String, bytes: []u8, } { // 1. select which bucket to allocate from const bucket = self.getBucketByLen(@intCast(length)); // 2. grab or recycle an allocation from that bucket const allocation = try bucket.assignOrRecycleSlot(self.getAllocator()); // 3. write in the string length. const strAllocation = StringAllocation.fromPtr(allocation.bytes.ptr); strAllocation.initRc(); strAllocation.setLen(length); return .{ .handle = .{ .bucketId = @intCast(bucket.id), .index = allocation.index }, .bytes = strAllocation.bytes(), }; } inline fn getBucketByLen(self: *@This(), stringLength: u32) *Bucket { const actualLength = stringLength + StringAllocation.HeaderLen; // theres probably a few bitwise operations that can do this lookup really // quickly.. too tired to think of them right now for (self.buckets.items) |*bucket| { if (bucket.allocSize >= actualLength) { return bucket; } } @panic("Unable to find a bucket to allocate string of length."); } inline fn getBucketFromHandle(self: @This(), handle: String) *Bucket { return &self.buckets.items[@intCast(handle.bucketId)]; } pub fn getUtf8(self: @This(), handle: String) []u8 { const x = self.handleToAllocation(handle); return x.bytes(); } pub fn rcAdd(self: *@This(), handle: String) void { const allocation = self.handleToAllocation(handle); allocation.rcAdd(); } pub fn handleToAllocation(self: @This(), handle: String) StringAllocation { const bucket = self.getBucketFromHandle(handle); const allocation = StringAllocation.fromPtr(bucket.getBytesFromIndex(handle.index).ptr); return allocation; } pub fn strDestroy(self: *@This(), handle: String) void { const allocation = self.handleToAllocation(handle); if (allocation.rcSub()) { const bucket = self.getBucketFromHandle(handle); bucket.destroySlot(self.getAllocator(), @intCast(handle.index)); } } pub fn destroy(self: *@This()) void { self.arena.deinit(); self.buckets.deinit(self.backingAllocator); self.backingAllocator.destroy(self); } }; pub fn setup(allocator: std.mem.Allocator) !void { gStringContext = try StringContext.create(allocator); } pub fn shutdown() void { gStringContext.destroy(); } test "string context" { try setup(std.testing.allocator); defer shutdown(); const ctx = gStringContext; const testSizes: []const u32 = &.{ 14, 15, 16, 12, 32, 10, 1, 2, 3, 4, 440, 450, 3200 }; for (testSizes) |testSize| { const results = try ctx.strNew(testSize); std.debug.print("test {d} = {d}, {d}\n", .{ testSize, results.handle.bucketId, results.handle.index }); } const L = struct { pub fn wrap(x: String) void { std.debug.print("{d},{d}\n", .{ x.bucketId, x.index }); } }; var s = try String.new("lmao2nova"); L.wrap(s); const x = s.utf8(); std.debug.print("{x}\n", .{x.len}); // doing 1Billion accesses { const now = std.time.nanoTimestamp(); for (0..1_000_000_000) |i| { _ = i; _ = s.utf8(); } const end = std.time.nanoTimestamp(); const duration = end - now; std.debug.print("time spent {d}ms\n", .{@as(f64, @floatFromInt(duration)) / 1000_000}); } { const now = std.time.nanoTimestamp(); // 1 million string creations/destructions for (0..1_000_000) |i| { _ = i; const y = try String.new("wutang forever\n"); defer y.drop(); } const end = std.time.nanoTimestamp(); const duration = end - now; std.debug.print("time spent {d}ms\n", .{@as(f64, @floatFromInt(duration)) / 1000_000}); } // allocate 1 million strings const s2 = s.pin(); defer s2.drop(); }