From 0c1fbc4ea6ff5c74b83d3959eac0b355570bd439 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Fri, 19 Sep 2025 17:48:07 -0700 Subject: [PATCH 1/2] std: remove loop from growCapacity I measured this against master branch and found no statistical difference. Since this code is simpler and logically superior due to always leaving sufficient unused capacity when growing, it is preferred over status quo. --- lib/std/Io/Writer.zig | 2 +- lib/std/array_list.zig | 17 ++++++----------- lib/std/deque.zig | 14 +------------- lib/std/multi_array_list.zig | 23 +++++++++-------------- 4 files changed, 17 insertions(+), 39 deletions(-) diff --git a/lib/std/Io/Writer.zig b/lib/std/Io/Writer.zig index 7f1b1fd884ebe9905db80baeae3204166bed8ed2..91028d60d0f590430aa4d2a812d575a7bc784da7 100644 --- a/lib/std/Io/Writer.zig +++ b/lib/std/Io/Writer.zig @@ -2661,7 +2661,7 @@ pub const Allocating = struct { pub fn ensureTotalCapacity(a: *Allocating, new_capacity: usize) Allocator.Error!void { // Protects growing unnecessarily since better_capacity will be larger. if (a.writer.buffer.len >= new_capacity) return; - const better_capacity = ArrayList(u8).growCapacity(a.writer.buffer.len, new_capacity); + const better_capacity = ArrayList(u8).growCapacity(new_capacity); return ensureTotalCapacityPrecise(a, better_capacity); } diff --git a/lib/std/array_list.zig b/lib/std/array_list.zig index 9718d19c7e37e0dc5cf22dceeacb4c322258b7f9..96a034444220d383f90ff93e4b3d372c8e0c21e3 100644 --- a/lib/std/array_list.zig +++ b/lib/std/array_list.zig @@ -172,7 +172,7 @@ pub fn AlignedManaged(comptime T: type, comptime alignment: ?mem.Alignment) type // a new buffer and doing our own copy. With a realloc() call, // the allocator implementation would pointlessly copy our // extra capacity. - const new_capacity = Aligned(T, alignment).growCapacity(self.capacity, new_len); + const new_capacity = Aligned(T, alignment).growCapacity(new_len); const old_memory = self.allocatedSlice(); if (self.allocator.remap(old_memory, new_capacity)) |new_memory| { self.items.ptr = new_memory.ptr; @@ -408,7 +408,7 @@ pub fn AlignedManaged(comptime T: type, comptime alignment: ?mem.Alignment) type // Protects growing unnecessarily since better_capacity will be larger. if (self.capacity >= new_capacity) return; - const better_capacity = Aligned(T, alignment).growCapacity(self.capacity, new_capacity); + const better_capacity = Aligned(T, alignment).growCapacity(new_capacity); return self.ensureTotalCapacityPrecise(better_capacity); } @@ -1160,7 +1160,7 @@ pub fn Aligned(comptime T: type, comptime alignment: ?mem.Alignment) type { /// Invalidates element pointers if additional memory is needed. pub fn ensureTotalCapacity(self: *Self, gpa: Allocator, new_capacity: usize) Allocator.Error!void { if (self.capacity >= new_capacity) return; - return self.ensureTotalCapacityPrecise(gpa, growCapacity(self.capacity, new_capacity)); + return self.ensureTotalCapacityPrecise(gpa, growCapacity(new_capacity)); } /// If the current capacity is less than `new_capacity`, this function will @@ -1359,17 +1359,12 @@ pub fn Aligned(comptime T: type, comptime alignment: ?mem.Alignment) type { return self.getLast(); } - const init_capacity = @as(comptime_int, @max(1, std.atomic.cache_line / @sizeOf(T))); + const init_capacity: comptime_int = @max(1, std.atomic.cache_line / @sizeOf(T)); /// Called when memory growth is necessary. Returns a capacity larger than /// minimum that grows super-linearly. - pub fn growCapacity(current: usize, minimum: usize) usize { - var new = current; - while (true) { - new +|= new / 2 + init_capacity; - if (new >= minimum) - return new; - } + pub fn growCapacity(minimum: usize) usize { + return minimum +| (minimum / 2 + init_capacity); } }; } diff --git a/lib/std/deque.zig b/lib/std/deque.zig index 7130cd33fe0b0033919c684ad1a066e1d54c0c3d..267b8a0afe4a7c40cb0250c0afbeb0b55bb61bc1 100644 --- a/lib/std/deque.zig +++ b/lib/std/deque.zig @@ -56,7 +56,7 @@ pub fn Deque(comptime T: type) type { /// Invalidates element pointers if additional memory is needed. pub fn ensureTotalCapacity(deque: *Self, gpa: Allocator, new_capacity: usize) Allocator.Error!void { if (deque.buffer.len >= new_capacity) return; - return deque.ensureTotalCapacityPrecise(gpa, growCapacity(deque.buffer.len, new_capacity)); + return deque.ensureTotalCapacityPrecise(gpa, std.ArrayList(T).growCapacity(new_capacity)); } /// If the current capacity is less than `new_capacity`, this function will @@ -243,18 +243,6 @@ pub fn Deque(comptime T: type) type { return index - head_len; } } - - const init_capacity: comptime_int = @max(1, std.atomic.cache_line / @sizeOf(T)); - - /// Called when memory growth is necessary. Returns a capacity larger than - /// minimum that grows super-linearly. - fn growCapacity(current: usize, minimum: usize) usize { - var new = current; - while (true) { - new +|= new / 2 + init_capacity; - if (new >= minimum) return new; - } - } }; } diff --git a/lib/std/multi_array_list.zig b/lib/std/multi_array_list.zig index e4eb60bd93c4aff016cc64a104f2fb08820d46f7..ec4b0b72e1c33baaf518b4eee35b833ffd275092 100644 --- a/lib/std/multi_array_list.zig +++ b/lib/std/multi_array_list.zig @@ -457,24 +457,19 @@ pub fn MultiArrayList(comptime T: type) type { /// Invalidates element pointers if additional memory is needed. pub fn ensureTotalCapacity(self: *Self, gpa: Allocator, new_capacity: usize) Allocator.Error!void { if (self.capacity >= new_capacity) return; - return self.setCapacity(gpa, growCapacity(self.capacity, new_capacity)); + return self.setCapacity(gpa, growCapacity(new_capacity)); } - const init_capacity = init: { - var max = 1; - for (fields) |field| max = @as(comptime_int, @max(max, @sizeOf(field.type))); - break :init @as(comptime_int, @max(1, std.atomic.cache_line / max)); + const init_capacity: comptime_int = init: { + var max: comptime_int = 1; + for (fields) |field| max = @max(max, @sizeOf(field.type)); + break :init @max(1, std.atomic.cache_line / max); }; - /// Called when memory growth is necessary. Returns a capacity larger than - /// minimum that grows super-linearly. - fn growCapacity(current: usize, minimum: usize) usize { - var new = current; - while (true) { - new +|= new / 2 + init_capacity; - if (new >= minimum) - return new; - } + /// Given a lower bound of required memory capacity, returns a larger value + /// with super-linear growth. + pub fn growCapacity(minimum: usize) usize { + return minimum +| (minimum / 2 + init_capacity); } /// Modify the array so that it can hold at least `additional_count` **more** items. -- 2.54.0 From 3cc0fc601af62adebf6b9f3bb1a241fa8a501d0b Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Sat, 20 Sep 2025 14:38:01 -0700 Subject: [PATCH 2/2] std.json: delete test tightly coupled to ArrayList growth This test works by assuming that std.ArrayList will grow with a specific capacity increasing pattern, which is an invalid assumption. Delete the offending test. --- lib/std/json/scanner_test.zig | 27 --------------------------- lib/std/json/static_test.zig | 2 +- 2 files changed, 1 insertion(+), 28 deletions(-) diff --git a/lib/std/json/scanner_test.zig b/lib/std/json/scanner_test.zig index ae954a88f0ccfb0e25a7e6c2b2b4de1e6c1c1054..5b4bfa532ae7660d16b0c2ce379fab53581bdc7a 100644 --- a/lib/std/json/scanner_test.zig +++ b/lib/std/json/scanner_test.zig @@ -431,33 +431,6 @@ test "skipValue" { try std.testing.expectError(error.SyntaxError, testSkipValue("[102, 111, 111}")); } -fn testEnsureStackCapacity(do_ensure: bool) !void { - var fail_alloc = std.testing.FailingAllocator.init(std.testing.allocator, .{ .fail_index = 1 }); - const failing_allocator = fail_alloc.allocator(); - - const nestings = 2049; // intentionally not a power of 2. - var input_string: std.ArrayListUnmanaged(u8) = .empty; - try input_string.appendNTimes(std.testing.allocator, '[', nestings); - try input_string.appendNTimes(std.testing.allocator, ']', nestings); - defer input_string.deinit(std.testing.allocator); - - var scanner = Scanner.initCompleteInput(failing_allocator, input_string.items); - defer scanner.deinit(); - - if (do_ensure) { - try scanner.ensureTotalStackCapacity(nestings); - } - - try scanner.skipValue(); - try std.testing.expectEqual(Token.end_of_document, try scanner.next()); -} -test "ensureTotalStackCapacity" { - // Once to demonstrate failure. - try std.testing.expectError(error.OutOfMemory, testEnsureStackCapacity(false)); - // Then to demonstrate it works. - try testEnsureStackCapacity(true); -} - fn testDiagnosticsFromSource(expected_error: ?anyerror, line: u64, col: u64, byte_offset: u64, source: anytype) !void { var diagnostics = Diagnostics{}; source.enableDiagnostics(&diagnostics); diff --git a/lib/std/json/static_test.zig b/lib/std/json/static_test.zig index 735ccd82e16fef5f05c42d98673fb8e79d9de95f..805d2840a1508620f228ed9afc49eeef3002d125 100644 --- a/lib/std/json/static_test.zig +++ b/lib/std/json/static_test.zig @@ -914,7 +914,7 @@ test "parse at comptime" { uptime: u64, }; const config = comptime x: { - var buf: [256]u8 = undefined; + var buf: [300]u8 = undefined; var fba = std.heap.FixedBufferAllocator.init(&buf); const res = parseFromSliceLeaky(Config, fba.allocator(), doc, .{}); // Assert no error can occur since we are -- 2.54.0