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:
Mitchell Hashimoto
2026-07-11 20:38:17 -07:00
parent 0f50fdf6ce
commit 4f35a0614e

View File

@@ -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| {