From 4f35a0614e94fbe2f81260c96b5398019d9db9c2 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 11 Jul 2026 20:38:17 -0700 Subject: [PATCH] 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% | --- src/terminal/bitmap_allocator.zig | 51 +++++++++++++++++++++++++------ 1 file changed, 42 insertions(+), 9 deletions(-) diff --git a/src/terminal/bitmap_allocator.zig b/src/terminal/bitmap_allocator.zig index 4e72fe29a..367409797 100644 --- a/src/terminal/bitmap_allocator.zig +++ b/src/terminal/bitmap_allocator.zig @@ -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| {