Skip to content

std.ArrayList: initial capacity based on cache line size #22865

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Feb 13, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 19 additions & 19 deletions lib/std/array_list.zig
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ pub fn ArrayListAligned(comptime T: type, comptime alignment: ?u29) 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 = growCapacity(self.capacity, new_len);
const new_capacity = ArrayListAlignedUnmanaged(T, alignment).growCapacity(self.capacity, new_len);
const old_memory = self.allocatedSlice();
if (self.allocator.remap(old_memory, new_capacity)) |new_memory| {
self.items.ptr = new_memory.ptr;
Expand Down Expand Up @@ -446,7 +446,7 @@ pub fn ArrayListAligned(comptime T: type, comptime alignment: ?u29) type {

if (self.capacity >= new_capacity) return;

const better_capacity = growCapacity(self.capacity, new_capacity);
const better_capacity = ArrayListAlignedUnmanaged(T, alignment).growCapacity(self.capacity, new_capacity);
return self.ensureTotalCapacityPrecise(better_capacity);
}

Expand Down Expand Up @@ -1062,14 +1062,12 @@ pub fn ArrayListAlignedUnmanaged(comptime T: type, comptime alignment: ?u29) typ
self.capacity = 0;
}

/// If the current capacity is less than `new_capacity`, this function will
/// modify the array so that it can hold at least `new_capacity` items.
/// Modify the array so that it can hold at least `new_capacity` items.
/// Implements super-linear growth to achieve amortized O(1) append operations.
/// Invalidates element pointers if additional memory is needed.
pub fn ensureTotalCapacity(self: *Self, allocator: Allocator, new_capacity: usize) Allocator.Error!void {
pub fn ensureTotalCapacity(self: *Self, gpa: Allocator, new_capacity: usize) Allocator.Error!void {
if (self.capacity >= new_capacity) return;

const better_capacity = growCapacity(self.capacity, new_capacity);
return self.ensureTotalCapacityPrecise(allocator, better_capacity);
return self.ensureTotalCapacityPrecise(gpa, growCapacity(self.capacity, new_capacity));
}

/// If the current capacity is less than `new_capacity`, this function will
Expand Down Expand Up @@ -1218,18 +1216,20 @@ pub fn ArrayListAlignedUnmanaged(comptime T: type, comptime alignment: ?u29) typ
if (self.items.len == 0) return null;
return self.getLast();
}
};
}

/// 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 + 8;
if (new >= minimum)
return new;
}
const init_capacity = @as(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;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think about also preferring capacities which are a multiple of the cache line size?

Suggested change
new +|= new / 2 + init_capacity;
new = std.mem.alignForward(usize, new +| new / 2 + init_capacity, init_capacity)

Copy link
Member Author

@andrewrk andrewrk Feb 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

worth a try. that expression needs work. probably align backward would be fewer instructions and avoid potential overflow

if (new >= minimum)
return new;
}
}
};
}

/// Integer addition returning `error.OutOfMemory` on overflow.
Expand Down
9 changes: 7 additions & 2 deletions lib/std/json/scanner_test.zig
Original file line number Diff line number Diff line change
Expand Up @@ -435,8 +435,13 @@ 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 = 999; // intentionally not a power of 2.
var scanner = JsonScanner.initCompleteInput(failing_allocator, "[" ** nestings ++ "]" ** nestings);
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 = JsonScanner.initCompleteInput(failing_allocator, input_string.items);
defer scanner.deinit();

if (do_ensure) {
Expand Down
2 changes: 1 addition & 1 deletion lib/std/json/static_test.zig
Original file line number Diff line number Diff line change
Expand Up @@ -916,7 +916,7 @@ test "parse at comptime" {
uptime: u64,
};
const config = comptime x: {
var buf: [32]u8 = undefined;
var buf: [256]u8 = undefined;
var fba = std.heap.FixedBufferAllocator.init(&buf);
const res = parseFromSliceLeaky(Config, fba.allocator(), doc, .{});
// Assert no error can occur since we are
Expand Down
28 changes: 19 additions & 9 deletions lib/std/multi_array_list.zig
Original file line number Diff line number Diff line change
Expand Up @@ -405,17 +405,27 @@ pub fn MultiArrayList(comptime T: type) type {

/// Modify the array so that it can hold at least `new_capacity` items.
/// Implements super-linear growth to achieve amortized O(1) append operations.
/// Invalidates pointers if additional memory is needed.
pub fn ensureTotalCapacity(self: *Self, gpa: Allocator, new_capacity: usize) !void {
var better_capacity = self.capacity;
if (better_capacity >= new_capacity) return;
/// 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));
}

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

/// 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) {
better_capacity += better_capacity / 2 + 8;
if (better_capacity >= new_capacity) break;
new +|= new / 2 + init_capacity;
if (new >= minimum)
return new;
}

return self.setCapacity(gpa, better_capacity);
}

/// Modify the array so that it can hold at least `additional_count` **more** items.
Expand Down Expand Up @@ -838,7 +848,7 @@ test "union" {

try testing.expectEqual(@as(usize, 0), list.items(.tags).len);

try list.ensureTotalCapacity(ally, 2);
try list.ensureTotalCapacity(ally, 3);

list.appendAssumeCapacity(.{ .a = 1 });
list.appendAssumeCapacity(.{ .b = "zigzag" });
Expand Down
2 changes: 1 addition & 1 deletion lib/std/zig/string_literal.zig
Original file line number Diff line number Diff line change
Expand Up @@ -377,7 +377,7 @@ test parseAlloc {
const expectError = std.testing.expectError;
const eql = std.mem.eql;

var fixed_buf_mem: [64]u8 = undefined;
var fixed_buf_mem: [512]u8 = undefined;
var fixed_buf_alloc = std.heap.FixedBufferAllocator.init(&fixed_buf_mem);
const alloc = fixed_buf_alloc.allocator();

Expand Down
Loading