terminal: reuse hashes across set insertion

Compute a RefCountedSet value hash once and pass it through lookup and
insertion. A miss previously hashed before probing and then repeated the
same work when insertion began. The shared private lookup path also keeps
zero-capacity contexts from being evaluated before their backing data exists.

ReleaseFast timings are seven-run median user CPU times.

| Workload | Before | After | Change |
| --- | ---: | ---: | ---: |
| 128 styles, live lookup | 0.75s | 0.69s | -8.0% |
| 128 styles, dead churn | 0.79s | 0.72s | -8.9% |
| 4,096 styles, dead churn | 0.85s | 0.77s | -9.4% |
| Alternating SGR stream | 1.13s | 1.11s | -1.8% |
| Clear/redraw SGR stream | 0.67s | 0.65s | -3.0% |
This commit is contained in:
Mitchell Hashimoto
2026-07-11 19:55:05 -07:00
parent b0cf99cf9a
commit b8458383c9

View File

@@ -271,6 +271,11 @@ pub fn RefCountedSet(
return try self.addContext(base, value, self.context);
}
pub fn addContext(self: *Self, base: anytype, value: T, ctx: Context) AddError!Id {
if (self.layout.table_cap == 0) {
@branchHint(.cold);
return AddError.OutOfMemory;
}
const items = self.items.ptr(base);
// Trim dead items from the end of the list.
@@ -279,8 +284,10 @@ pub fn RefCountedSet(
self.deleteItem(base, self.next_id, ctx);
}
const hash: u64 = ctx.hash(value);
// If the item already exists, return it.
if (self.lookupContext(base, value, ctx)) |id| {
if (self.lookupAdaptedHash(base, value, ctx, hash)) |id| {
// Notify the context that the value is "deleted" because
// we're reusing the existing value in the set. This allows
// callers to clean up any resources associated with the value.
@@ -320,7 +327,7 @@ pub fn RefCountedSet(
return AddError.OutOfMemory;
}
const id = self.insert(base, value, self.next_id, ctx);
const id = self.insert(base, value, self.next_id, ctx, hash);
items[id].meta.ref += 1;
assert(items[id].meta.ref == 1);
self.living += 1;
@@ -536,20 +543,26 @@ pub fn RefCountedSet(
/// Look up a value using a key and context whose hash and equality
/// functions adapt that key to resident values of type T.
pub fn lookupAdapted(self: *const Self, base: anytype, value: anytype, ctx: anytype) ?Id {
// A zero-capacity set (a valid special case of Layout.init)
// contains nothing and has a zero-size table, so we can't
// probe it: table[0] would read whatever memory follows the
// set in the backing buffer.
// A zero-capacity set has no table to probe. Check before hashing
// because adapted contexts may require initialized backing data.
if (self.layout.table_cap == 0) {
@branchHint(.cold);
return null;
}
return self.lookupAdaptedHash(base, value, ctx, ctx.hash(value));
}
fn lookupAdaptedHash(
self: *const Self,
base: anytype,
value: anytype,
ctx: anytype,
hash: u64,
) ?Id {
const table = self.table.ptr(base);
const items = self.items.ptr(base);
const hash: u64 = ctx.hash(value);
for (0..self.max_psl + 1) |i| {
const p: usize = @intCast((hash +% i) & self.layout.table_mask);
const id = table[p];
@@ -591,8 +604,10 @@ pub fn RefCountedSet(
/// be used as the ID. If an existing item is found, the `new_id`
/// is ignored and the existing item's ID is returned.
fn upsert(self: *Self, base: anytype, value: T, new_id: Id, ctx: Context) Id {
const hash: u64 = ctx.hash(value);
// If the item already exists, return it.
if (self.lookupContext(base, value, ctx)) |id| {
if (self.lookupAdaptedHash(base, value, ctx, hash)) |id| {
// Notify the context that the value is "deleted" because
// we're reusing the existing value in the set. This allows
// callers to clean up any resources associated with the value.
@@ -601,16 +616,23 @@ pub fn RefCountedSet(
return id;
}
return self.insert(base, value, new_id, ctx);
return self.insert(base, value, new_id, ctx, hash);
}
/// Insert the given value into the hash table with the given ID.
///
/// If runtime safety is enabled, asserts that
/// the value is not already present in the table.
fn insert(self: *Self, base: anytype, value: T, new_id: Id, ctx: Context) Id {
fn insert(
self: *Self,
base: anytype,
value: T,
new_id: Id,
ctx: Context,
hash: u64,
) Id {
if (comptime std.debug.runtime_safety)
assert(self.lookupContext(base, value, ctx) == null);
assert(self.lookupAdaptedHash(base, value, ctx, hash) == null);
const table = self.table.ptr(base);
const items = self.items.ptr(base);
@@ -621,8 +643,6 @@ pub fn RefCountedSet(
.meta = .{ .psl = 0, .ref = 0 },
};
const hash: u64 = ctx.hash(value);
var held_id: Id = new_id;
var held_item: *Item = &new_item;
@@ -835,6 +855,25 @@ test "RefCountedSet random operations against live-value oracle" {
}
}
test "RefCountedSet zero capacity bypasses context" {
const testing = std.testing;
const Context = struct {
pub fn hash(_: *const @This(), _: u8) u64 {
unreachable;
}
pub fn eql(_: *const @This(), _: u8, _: u8) bool {
unreachable;
}
};
const Set = RefCountedSet(u8, u8, u16, Context);
var buf: [0]u8 = .{};
var set = Set.init(.init(&buf), .init(0), .{});
try testing.expectEqual(null, set.lookup(&buf, 1));
try testing.expectError(error.OutOfMemory, set.add(&buf, 1));
}
test "RefCountedSet empty marker at maximum table capacity" {
const testing = std.testing;
const Context = struct {