mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-07-31 12:49:03 +00:00
terminal: fold bitmap run searches logarithmically
Finding an in-word run of n free chunks previously intersected the bitmap with every shift from one through n minus one. Large grapheme allocations therefore performed up to 63 dependent shift-and-mask steps for every word examined. Fold each intermediate result by its represented run length instead. This preserves the cross-word first-fit fallback while reducing an in-word search to logarithmic steps. ReleaseFast medians use nine sequential runs after two warmups and report user CPU time. Grapheme churn uses 256 cells at the production 80% map limit; loop counts are calibrated per row. | extra codepoints | chunks | loops | before | after | change | |---:|---:|---:|---:|---:|---:| | 1 | 1 | 65,535 | 289.506 ms | 289.121 ms | -0.1% | | 5 | 2 | 65,535 | 210.946 ms | 188.005 ms | -10.9% | | 13 | 4 | 50,000 | 234.318 ms | 192.031 ms | -18.0% | | 29 | 8 | 25,000 | 207.009 ms | 142.711 ms | -31.1% | | 125 | 32 | 3,000 | 157.674 ms | 71.398 ms | -54.7% | | 253 | 64 | 800 | 165.088 ms | 43.548 ms | -73.6% | Real VT stream medians use seven sequential runs after two warmups. | workload | before | after | change | |---|---:|---:|---:| | VT grapheme, 5 extra codepoints | 448.057 ms | 429.095 ms | -4.2% | | VT grapheme, 32 extra codepoints | 2.005 s | 1.514 s | -24.5% |
This commit is contained in:
@@ -310,23 +310,33 @@ fn findFreeChunks(bitmaps: []u64, n: usize) ?usize {
|
||||
|
||||
assert(n <= @bitSizeOf(u64));
|
||||
for (bitmaps, 0..) |*bitmap, idx| {
|
||||
// Shift the bitmap to find `n` sequential free chunks.
|
||||
// Fold the bitmap onto itself to find `n` sequential free chunks.
|
||||
// Each fold extends the represented run by h, so doubling the
|
||||
// represented run takes O(log n) steps rather than n - 1.
|
||||
// EXAMPLE:
|
||||
// n = 4
|
||||
// shifted = 001111001011110010
|
||||
// & 000111100101111001
|
||||
// & 000011110010111100
|
||||
// & 000001111001011110
|
||||
// shifted = 001111001011110010 (runs of 1)
|
||||
// & 000111100101111001 (runs of 2)
|
||||
// = 000111000001110000
|
||||
// & 000001110000011100 (runs of 4)
|
||||
// = 000001000000010000
|
||||
// ^ ^
|
||||
// In this example there are 2 places with at least 4 sequential 1s.
|
||||
var shifted: u64 = bitmap.*;
|
||||
for (1..n) |i| shifted &= bitmap.* >> @intCast(i);
|
||||
const run: u64 = run: {
|
||||
var shifted: u64 = bitmap.*;
|
||||
var len: usize = 1;
|
||||
while (len < n) {
|
||||
const h = @min(len, n - len);
|
||||
shifted &= shifted >> @intCast(h);
|
||||
len += h;
|
||||
}
|
||||
break :run shifted;
|
||||
};
|
||||
|
||||
if (shifted != 0) {
|
||||
if (run != 0) {
|
||||
// Trailing zeroes gets us the index of the first bit index with at
|
||||
// least `n` sequential 1s. In the example above, that would be `4`.
|
||||
const bit = @ctz(shifted);
|
||||
const bit = @ctz(run);
|
||||
|
||||
// Calculate the mask so we can mark it as used.
|
||||
const mask = (@as(u64, std.math.maxInt(u64)) >> @intCast(64 - n)) << @intCast(bit);
|
||||
@@ -407,6 +417,21 @@ test "findFreeChunks exactly 64 chunks" {
|
||||
try testing.expectEqual(@as(usize, 0), idx);
|
||||
}
|
||||
|
||||
test "findFreeChunks folded in-word search is exhaustive" {
|
||||
const testing = std.testing;
|
||||
|
||||
for (1..65) |n| {
|
||||
for (0..65 - n) |start| {
|
||||
var bitmaps = [_]u64{
|
||||
(@as(u64, std.math.maxInt(u64)) >> @intCast(64 - n)) <<
|
||||
@intCast(start),
|
||||
};
|
||||
try testing.expectEqual(start, findFreeChunks(&bitmaps, n).?);
|
||||
try testing.expectEqual(@as(u64, 0), bitmaps[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test "findFreeChunks small allocation across bitmap boundary" {
|
||||
const testing = std.testing;
|
||||
|
||||
@@ -503,6 +528,9 @@ test "findFreeChunks larger than 64 chunks exact" {
|
||||
|
||||
test "findFreeChunks random operations against bit oracle" {
|
||||
const testing = std.testing;
|
||||
|
||||
// Search individual bits instead of sharing any of findFreeChunks' word
|
||||
// folding or cross-word logic. This is deliberately simple and slow.
|
||||
const Oracle = struct {
|
||||
fn find(bitmaps: []const u64, n: usize) ?usize {
|
||||
var run: usize = 0;
|
||||
@@ -523,10 +551,15 @@ test "findFreeChunks random operations against bit oracle" {
|
||||
var prng = std.Random.DefaultPrng.init(0xB17);
|
||||
const random = prng.random();
|
||||
for (0..10_000) |_| {
|
||||
// Random words and requests through 96 chunks exercise both the
|
||||
// single-word and multi-word implementations.
|
||||
var bitmaps: [4]u64 = undefined;
|
||||
for (&bitmaps) |*bitmap| bitmap.* = random.int(u64);
|
||||
var expected = bitmaps;
|
||||
const n = random.intRangeAtMost(usize, 1, 96);
|
||||
|
||||
// Apply the oracle's allocation to a separate copy so both the
|
||||
// returned offset and the mutated bitmap state can be compared.
|
||||
const expected_idx = Oracle.find(&bitmaps, n);
|
||||
if (expected_idx) |start| {
|
||||
for (start..start + n) |i| {
|
||||
|
||||
Reference in New Issue
Block a user