A poolable string builder (aka string buffer) for Zig
0

Configure Feed

Select the types of activity you want to include in your feed.

ability to acquire a builder with a allocation specifically for dynamic allocation

Karl Seguin (Jun 24, 2023, 8:49 PM +0800) f55f0d9e 00835730

+43 -6
+2
readme.md
··· 38 38 ``` 39 39 40 40 The `Pool` is thread-safe. The `StringBuilder` is not thread safe. 41 + 42 + For a more advanced use case, `pool.acquireWithAllocator(std.mem.Allocator)` can be used. This has a specific purpose: to allocate the static buffer upfront using [probably] a general purpose allocator, but for any dynamic allocation to happen with a [probably] arena allocator. This is meant for the case where an arena allocator is available which outlives the checked out buffer. In such cases, using the arena allocator for any potential dynamic allocation by the buffer will offer better performance.
+21
src/pool.zig
··· 41 41 } 42 42 43 43 pub fn acquire(self: *Pool) !*StringBuilder { 44 + return self.acquireWithAllocator(self.allocator); 45 + } 46 + 47 + pub fn acquireWithAllocator(self: *Pool, dyn_allocator: Allocator) !*StringBuilder { 44 48 self.mutex.lock(); 45 49 46 50 const builders = self.builders; ··· 53 57 const sb = try allocator.create(StringBuilder); 54 58 sb.* = try StringBuilder.init(allocator, self.builder_size); 55 59 if (comptime builtin.is_test) sb.buf[0] = 0; 60 + sb._da = dyn_allocator; 56 61 return sb; 57 62 } 58 63 const index = available - 1; 59 64 const sb = builders[index]; 60 65 self.available = index; 61 66 self.mutex.unlock(); 67 + sb._da = dyn_allocator; 62 68 return sb; 63 69 } 64 70 ··· 101 107 p.release(sb3a); 102 108 p.release(sb2a); 103 109 p.release(sb1b); 110 + } 111 + 112 + test "pool: dynamic allocator" { 113 + var p = try Pool.init(t.allocator, 2, 5); 114 + defer p.deinit(); 115 + 116 + var arena = std.heap.ArenaAllocator.init(t.allocator); 117 + defer arena.deinit(); 118 + 119 + var sb = p.acquireWithAllocator(arena.allocator()) catch unreachable; 120 + try sb.write("hello world how's it going?"); 121 + try sb.write("he"); 122 + try sb.write("hello world"); 123 + try sb.write("are you doing well? I hope so, I don't love how this is being implemented, but I think the feature is worthwhile"); 124 + p.release(sb); 104 125 } 105 126 106 127 test "pool: threadsafety" {
+20 -6
src/string_builder.zig
··· 4 4 pub const Pool = @import("pool.zig").Pool; 5 5 6 6 pub const StringBuilder = struct { 7 - allocator: Allocator, 7 + // Two allocators! This is largely a feature meant to be used with the Pool. 8 + // Imagine you have a pool of 100 StringBuilders. Each one has a static buffer 9 + // of 2K, allocated with a general purpose allocator. We store that in _a. 10 + // Now you acquire an SB and start to write. You write more than 2K, so we 11 + // need to allocate `dynamic`. Yes, we could use our general purpose allocator 12 + // (aka _a), but what if the app would like to use a different allocator for 13 + // that, like an Arena? 14 + // Thus, `static` is always allocated with _a, and apps can opt to use a 15 + // different allocator, _da, to manage `dynamic`. `_da` is meant to be set 16 + // via pool.acquireWithAllocator since we expect _da to be transient. 17 + _a: Allocator, 18 + 19 + _da: ?Allocator, 8 20 9 21 // where in buf we currently are 10 22 pos: usize, ··· 21 33 pub fn init(allocator: Allocator, size: usize) !StringBuilder { 22 34 const static = try allocator.alloc(u8, size); 23 35 return .{ 36 + ._a = allocator, 37 + ._da = null, 24 38 .pos = 0, 25 39 .dynamic = null, 26 40 .buf = static, 27 41 .static = static, 28 - .allocator = allocator, 29 42 }; 30 43 } 31 44 32 45 pub fn deinit(self: StringBuilder) void { 33 - const allocator = self.allocator; 46 + const allocator = self._a; 34 47 allocator.free(self.static); 35 48 if (self.dynamic) |dyn| { 36 - allocator.free(dyn); 49 + (self._da orelse allocator).free(dyn); 37 50 } 38 51 } 39 52 40 53 pub fn reset(self: *StringBuilder) void { 41 54 self.pos = 0; 42 55 if (self.dynamic) |dyn| { 43 - self.allocator.free(dyn); 56 + (self._da orelse self._a).free(dyn); 44 57 self.dynamic = null; 45 58 self.buf = self.static; 46 59 } 60 + self._da = null; 47 61 } 48 62 49 63 pub fn len(self: StringBuilder) usize { ··· 113 127 if (new_capacity >= required_capacity) break; 114 128 } 115 129 116 - const allocator = self.allocator; 130 + const allocator = self._da orelse self._a; 117 131 if (buf.ptr == self.static.ptr or !allocator.resize(buf, new_capacity)) { 118 132 const new_buffer = try allocator.alloc(u8, new_capacity); 119 133 std.mem.copyForwards(u8, new_buffer[0..buf.len], buf);