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 {...@@ -270,18 +270,18 @@ fn parseTreeMode(input: string) !Tree.Object.Mode {
270}270}
271271
272pub const Tree = struct {272pub const Tree = struct {
273 children: std.MultiArrayList(Object).Slice,273 children: MultiArrayList(Object),
274274
275 pub fn get(self: Tree, name: string) ?Object {275 pub fn get(self: Tree, name: string) ?Object {
276 if (self.children.len <= 1000) {276 if (self.children.len() <= 1000) {
277 for (self.children.items(.name), 0..) |a_name, i| {277 for (self.children.items.name, 0..) |a_name, i| {
278 if (std.mem.eql(u8, a_name, name)) {278 if (std.mem.eql(u8, a_name, name)) {
279 return self.children.get(i);279 return self.children.get(i);
280 }280 }
281 }281 }
282 return null;282 return null;
283 }283 }
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;
285 return self.children.get(i);285 return self.children.get(i);
286 }286 }
287287
...@@ -292,7 +292,7 @@ pub const Tree = struct {...@@ -292,7 +292,7 @@ pub const Tree = struct {
292 }292 }
293293
294 pub fn find(self: Tree, name: string) ?Object {294 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| {
296 if (std.ascii.eqlIgnoreCase(item, name)) {296 if (std.ascii.eqlIgnoreCase(item, name)) {
297 return self.children.get(i);297 return self.children.get(i);
298 }298 }
...@@ -1196,12 +1196,11 @@ pub const Repository = struct {...@@ -1196,12 +1196,11 @@ pub const Repository = struct {
1196 }1196 }
1197 if (try r.getObject(arena, id.id)) |obj| {1197 if (try r.getObject(arena, id.id)) |obj| {
1198 if (obj.type == .tree) {1198 if (obj.type == .tree) {
1199 const MAL = std.MultiArrayList(Tree.Object);1199 const MAL = MultiArrayList(Tree.Object);
1200 var children: MAL = .empty;1200 var children: MAL = .empty;
1201 errdefer children.deinit(r.gpa);1201 errdefer children.deinit(r.gpa);
1202 try children.ensureUnusedCapacity(r.gpa, 33);1202 try children.ensureUnusedCapacity(r.gpa, 33);
1203 var i: usize = 0;1203 var i: usize = 0;
1204 var slice = children.slice();
1205 while (i < obj.content.len) {1204 while (i < obj.content.len) {
1206 const mode_end = std.mem.indexOfScalar(u8, obj.content[i..], ' ').?;1205 const mode_end = std.mem.indexOfScalar(u8, obj.content[i..], ' ').?;
1207 const mode = obj.content[i..][0..mode_end];1206 const mode = obj.content[i..][0..mode_end];
...@@ -1220,15 +1219,7 @@ pub const Repository = struct {...@@ -1220,15 +1219,7 @@ pub const Repository = struct {
1220 @memcpy(mode_buf[6 - mode.len ..], mode);1219 @memcpy(mode_buf[6 - mode.len ..], mode);
1221 const mode_real = try parseTreeMode(&mode_buf);1220 const mode_real = try parseTreeMode(&mode_buf);
12221221
1223 const j = slice.len;1222 try children.append(r.gpa, .{
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, .{
1232 .mode = mode_real,1223 .mode = mode_real,
1233 .name = name,1224 .name = name,
1234 .id = switch (mode_real.type) {1225 .id = switch (mode_real.type) {
...@@ -1245,21 +1236,20 @@ pub const Repository = struct {...@@ -1245,21 +1236,20 @@ pub const Repository = struct {
1245 list: *MAL,1236 list: *MAL,
12461237
1247 pub fn lessThan(s: @This(), a: usize, b: usize) bool {1238 pub fn lessThan(s: @This(), a: usize, b: usize) bool {
1248 const items = s.list.items(.name);1239 const items = s.list.items.name;
1249 return std.mem.order(u8, items[a], items[b]) == .lt;1240 return std.mem.order(u8, items[a], items[b]) == .lt;
1250 }1241 }
1251 pub fn swap(s: @This(), a: usize, b: usize) void {1242 pub fn swap(s: @This(), a: usize, b: usize) void {
1252 // Remove after updating to Zig 0.17. Ref: https://codeberg.org/ziglang/zig/pulls/320161243 // Remove after updating to Zig 0.17. Ref: https://codeberg.org/ziglang/zig/pulls/32016
1253 inline for (@typeInfo(Tree.Object).@"struct".fields) |field| {1244 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);
1255 std.mem.swap(@FieldType(Tree.Object, field.name), &its[a], &its[b]);1246 std.mem.swap(@FieldType(Tree.Object, field.name), &its[a], &its[b]);
1256 }1247 }
1257 }1248 }
1258 };1249 };
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() };1252 const tree: Tree = .{ .children = children };
1262 errdefer tree.children.deinit(r.gpa);
1263 try r.trees.put(r.gpa, id.id, tree);1253 try r.trees.put(r.gpa, id.id, tree);
1264 return .{ id, tree };1254 return .{ id, tree };
1265 }1255 }
...@@ -1371,12 +1361,12 @@ pub const Repository = struct {...@@ -1371,12 +1361,12 @@ pub const Repository = struct {
1371 const base_idx = try r.getCommitA(arena, base_oid.id);1361 const base_idx = try r.getCommitA(arena, base_oid.id);
1372 const base = base_idx.reify(r);1362 const base = base_idx.reify(r);
1373 const base_tree = (try traverseTo(r, arena, base.tree, dir_path)).?;1363 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
1376 var found: usize = 0;1366 var found: usize = 0;
1377 var result: std.StringArrayHashMapUnmanaged(CommitId) = .empty;1367 var result: std.StringArrayHashMapUnmanaged(CommitId) = .empty;
1378 defer result.deinit(r.gpa);1368 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
1381 var set: std.bit_set.DynamicBitSetUnmanaged = try .initEmpty(r.gpa, total);1371 var set: std.bit_set.DynamicBitSetUnmanaged = try .initEmpty(r.gpa, total);
1382 defer set.deinit(r.gpa);1372 defer set.deinit(r.gpa);
...@@ -1502,3 +1492,116 @@ fn traverseTo(r: *Repository, arena: std.mem.Allocator, treestart_id: TreeId, di...@@ -1502,3 +1492,116 @@ fn traverseTo(r: *Repository, arena: std.mem.Allocator, treestart_id: TreeId, di
1502 }1492 }
1503 return tree;1493 return tree;
1504}1494}
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}