authorgravatar for hello@nektro.netMeghan Denny <hello@nektro.net> 2026-04-22 00:52:50 -07:00
committergravatar for hello@nektro.netMeghan Denny <hello@nektro.net> 2026-04-22 00:52:50 -07:00
logb080a6170e933ba01bf18b5bcc335c5d6c7e8fbe
treeac4a82e8a13ec3af4a352b75ce83e092cdc6309b
parentc452ffcce8cabc43f24045f0fb062bcd14700be6
signaturebadge-check Signed by SSH key SHA256:4hHJbtBRU58AYXwjL7fkz2fnQHdiye8x1QpTCQ0sHNw

getTree: another speedup from a custom MultiArrayList


1 files changed, 126 insertions(+), 23 deletions(-)

git.zig+126-23
......@@ -270,18 +270,18 @@ fn parseTreeMode(input: string) !Tree.Object.Mode {
270270}
271271
272272pub const Tree = struct {
273 children: std.MultiArrayList(Object).Slice,
273 children: MultiArrayList(Object),
274274
275275 pub fn get(self: Tree, name: string) ?Object {
276 if (self.children.len <= 1000) {
277 for (self.children.items(.name), 0..) |a_name, i| {
276 if (self.children.len() <= 1000) {
277 for (self.children.items.name, 0..) |a_name, i| {
278278 if (std.mem.eql(u8, a_name, name)) {
279279 return self.children.get(i);
280280 }
281281 }
282282 return null;
283283 }
284 const i = std.sort.binarySearch([]const u8, self.children.items(.name), name, extras.compareFnSlice(u8)) orelse return null;
284 const i = std.sort.binarySearch([]const u8, self.children.items.name, name, extras.compareFnSlice(u8)) orelse return null;
285285 return self.children.get(i);
286286 }
287287
......@@ -292,7 +292,7 @@ pub const Tree = struct {
292292 }
293293
294294 pub fn find(self: Tree, name: string) ?Object {
295 for (self.children.items(.name), 0..) |item, i| {
295 for (self.children.items.name, 0..) |item, i| {
296296 if (std.ascii.eqlIgnoreCase(item, name)) {
297297 return self.children.get(i);
298298 }
......@@ -1196,12 +1196,11 @@ pub const Repository = struct {
11961196 }
11971197 if (try r.getObject(arena, id.id)) |obj| {
11981198 if (obj.type == .tree) {
1199 const MAL = std.MultiArrayList(Tree.Object);
1199 const MAL = MultiArrayList(Tree.Object);
12001200 var children: MAL = .empty;
12011201 errdefer children.deinit(r.gpa);
12021202 try children.ensureUnusedCapacity(r.gpa, 33);
12031203 var i: usize = 0;
1204 var slice = children.slice();
12051204 while (i < obj.content.len) {
12061205 const mode_end = std.mem.indexOfScalar(u8, obj.content[i..], ' ').?;
12071206 const mode = obj.content[i..][0..mode_end];
......@@ -1220,15 +1219,7 @@ pub const Repository = struct {
12201219 @memcpy(mode_buf[6 - mode.len ..], mode);
12211220 const mode_real = try parseTreeMode(&mode_buf);
12221221
1223 const j = slice.len;
1224 if (j == slice.capacity) {
1225 @branchHint(.cold);
1226 try children.ensureUnusedCapacity(r.gpa, 1);
1227 slice = children.slice();
1228 }
1229 children.len += 1;
1230 slice.len += 1;
1231 slice.set(j, .{
1222 try children.append(r.gpa, .{
12321223 .mode = mode_real,
12331224 .name = name,
12341225 .id = switch (mode_real.type) {
......@@ -1245,21 +1236,20 @@ pub const Repository = struct {
12451236 list: *MAL,
12461237
12471238 pub fn lessThan(s: @This(), a: usize, b: usize) bool {
1248 const items = s.list.items(.name);
1239 const items = s.list.items.name;
12491240 return std.mem.order(u8, items[a], items[b]) == .lt;
12501241 }
12511242 pub fn swap(s: @This(), a: usize, b: usize) void {
12521243 // Remove after updating to Zig 0.17. Ref: https://codeberg.org/ziglang/zig/pulls/32016
12531244 inline for (@typeInfo(Tree.Object).@"struct".fields) |field| {
1254 const its = s.list.items(@field(MAL.Field, field.name));
1245 const its = @field(s.list.items, field.name);
12551246 std.mem.swap(@FieldType(Tree.Object, field.name), &its[a], &its[b]);
12561247 }
12571248 }
12581249 };
1259 if (children.len > 1000) std.mem.sortContext(0, children.len, S{ .list = &children });
1250 if (children.len() > 1000) std.mem.sortContext(0, children.len(), S{ .list = &children });
12601251
1261 var tree: Tree = .{ .children = children.toOwnedSlice() };
1262 errdefer tree.children.deinit(r.gpa);
1252 const tree: Tree = .{ .children = children };
12631253 try r.trees.put(r.gpa, id.id, tree);
12641254 return .{ id, tree };
12651255 }
......@@ -1371,12 +1361,12 @@ pub const Repository = struct {
13711361 const base_idx = try r.getCommitA(arena, base_oid.id);
13721362 const base = base_idx.reify(r);
13731363 const base_tree = (try traverseTo(r, arena, base.tree, dir_path)).?;
1374 const total = base_tree.children.len;
1364 const total = base_tree.children.len();
13751365
13761366 var found: usize = 0;
13771367 var result: std.StringArrayHashMapUnmanaged(CommitId) = .empty;
13781368 defer result.deinit(r.gpa);
1379 for (base_tree.children.items(.name)) |name| try result.put(r.gpa, name, undefined);
1369 for (base_tree.children.items.name) |name| try result.put(r.gpa, name, undefined);
13801370
13811371 var set: std.bit_set.DynamicBitSetUnmanaged = try .initEmpty(r.gpa, total);
13821372 defer set.deinit(r.gpa);
......@@ -1502,3 +1492,116 @@ fn traverseTo(r: *Repository, arena: std.mem.Allocator, treestart_id: TreeId, di
15021492 }
15031493 return tree;
15041494}
1495
1496/// Variant on std.MultiArrayList but using an allocation per-field rather than a single for the entire structure.
1497fn MultiArrayList(T: type) type {
1498 return struct {
1499 items: Items,
1500 capacity: usize,
1501
1502 comptime Elem: type = T,
1503
1504 pub const empty: Self = .{
1505 .items = .{},
1506 .capacity = 0,
1507 };
1508
1509 pub const Self = @This();
1510
1511 const info = @typeInfo(T).@"struct";
1512
1513 pub const Items = blk: {
1514 var fields: [info.fields.len]std.builtin.Type.StructField = undefined;
1515 for (info.fields, 0..) |f, i| {
1516 const empty_slice: []f.type = &[_]f.type{};
1517 fields[i] = .{
1518 .name = f.name,
1519 .type = []f.type,
1520 .default_value_ptr = @ptrCast(&empty_slice),
1521 .is_comptime = false,
1522 .alignment = @alignOf(f.type),
1523 };
1524 }
1525 const _fields = fields;
1526 break :blk @Type(.{
1527 .@"struct" = .{
1528 .layout = .auto,
1529 .fields = &_fields,
1530 .decls = &.{},
1531 .is_tuple = false,
1532 },
1533 });
1534 };
1535
1536 pub fn deinit(self: *const Self, gpa: std.mem.Allocator) void {
1537 inline for (info.fields) |f| {
1538 gpa.free(@field(self.items, f.name).ptr[0..self.capacity]);
1539 }
1540 }
1541
1542 pub fn len(self: *const Self) usize {
1543 return @field(self.items, info.fields[0].name).len;
1544 }
1545
1546 pub fn get(self: *const Self, index: usize) T {
1547 var result: T = undefined;
1548 inline for (info.fields) |f| {
1549 @field(result, f.name) = @field(self.items, f.name)[index];
1550 }
1551 return result;
1552 }
1553
1554 pub fn ensureUnusedCapacity(self: *Self, gpa: std.mem.Allocator, additional_count: usize) !void {
1555 return self.ensureTotalCapacity(gpa, self.len() + additional_count);
1556 }
1557
1558 pub fn ensureTotalCapacity(self: *Self, gpa: std.mem.Allocator, new_capacity: usize) !void {
1559 if (self.capacity >= new_capacity) return;
1560 const actual_new_capacity = growCapacity(self.capacity, new_capacity);
1561 const previous_len = self.len();
1562 var new_items: Items = .{};
1563 inline for (info.fields, 0..) |f, i| {
1564 errdefer inline for (info.fields[0..i]) |g| gpa.free(@field(new_items, g.name)[0..actual_new_capacity]);
1565 @field(new_items, f.name) = try gpa.alloc(f.type, actual_new_capacity);
1566 @field(new_items, f.name).len = previous_len;
1567 }
1568 inline for (info.fields) |f| {
1569 @memcpy(@field(new_items, f.name)[0..previous_len], @field(self.items, f.name)[0..previous_len]);
1570 }
1571 inline for (info.fields) |f| {
1572 gpa.free(@field(self.items, f.name)[0..previous_len]);
1573 }
1574 self.items = new_items;
1575 self.capacity = actual_new_capacity;
1576 }
1577
1578 /// Called when memory growth is necessary. Returns a capacity larger than minimum that grows super-linearly.
1579 fn growCapacity(current: usize, minimum: usize) usize {
1580 var new = current;
1581 while (true) {
1582 new +|= new / 2 + init_capacity;
1583 if (new >= minimum) return new;
1584 }
1585 }
1586
1587 const init_capacity = init: {
1588 var max = 1;
1589 for (info.fields) |field| max = @as(comptime_int, @max(max, @sizeOf(field.type)));
1590 break :init @as(comptime_int, @max(1, std.atomic.cache_line / max));
1591 };
1592
1593 pub fn append(self: *Self, gpa: std.mem.Allocator, elem: T) !void {
1594 try self.ensureUnusedCapacity(gpa, 1);
1595 self.appendAssumeCapacity(elem);
1596 }
1597
1598 pub fn appendAssumeCapacity(self: *Self, elem: T) void {
1599 const plen = self.len();
1600 std.debug.assert(plen < self.capacity);
1601 inline for (info.fields) |f| {
1602 @field(self.items, f.name).len += 1;
1603 @field(self.items, f.name)[plen] = @field(elem, f.name);
1604 }
1605 }
1606 };
1607}