terminal: vectorize APC payload scanning

Kitty graphics payloads are dispatched in bulk, but finding each slice
boundary still examines every byte with a scalar loop. This leaves large
direct base64 image transmissions parser-bound.

Scan ordinary APC bytes using the vector width recommended for the compile
target. Keep the scalar scan both as the tail and as the full fallback when
the target has no recommended vector width. Test state-machine boundaries
against byte-at-a-time parsing.

A ReleaseFast APC parser benchmark over the same 64 MiB Kitty graphics
corpus, with 10 warmups and 30 measured runs, produced:

                  mean       median
  scalar        37.6 ms      32.6 ms
  vectorized    22.3 ms      19.0 ms

Hyperfine reports the vectorized version as 1.69 times faster overall, with
the median runtime improving by approximately 42 percent.
This commit is contained in:
Tim Culverhouse
2026-07-10 09:24:05 -05:00
parent 3f2b7946d7
commit 8c523ed039

View File

@@ -793,6 +793,18 @@ pub fn Stream(comptime H: type) type {
assert(self.parser.state == .sos_pm_apc_string);
var end: usize = 0;
if (comptime std.simd.suggestVectorLength(u8)) |vector_len| {
const ByteVector = @Vector(vector_len, u8);
while (end + vector_len <= input.len) {
const bytes: ByteVector = input[end..][0..vector_len].*;
const invalid = (bytes == @as(ByteVector, @splat(0x18))) |
(bytes == @as(ByteVector, @splat(0x1A))) |
(bytes == @as(ByteVector, @splat(0x1B))) |
(bytes >= @as(ByteVector, @splat(0x80)));
if (@reduce(.Or, invalid)) break;
end += vector_len;
}
}
while (end < input.len) {
switch (input[end]) {
// Not apc_put bytes: CAN/SUB/ESC and most C1 exit
@@ -3899,11 +3911,14 @@ test "stream: apc aborted by CAN" {
// CAN (0x18) aborts the APC string via the anywhere => ground
// transition. Exiting the sos_pm_apc_string state emits apc_end,
// and the trailing bytes are printed, not treated as APC data.
s.nextSlice("\x1b_Gabc\x18def");
s.nextSlice("\x1b_Gabcdefghijklmnopqrstuvwxyz0123456789\x18def");
try testing.expectEqual(@as(usize, 1), s.handler.started);
try testing.expectEqual(@as(usize, 1), s.handler.ended);
try testing.expectEqualStrings("Gabc", s.handler.buf[0..s.handler.len]);
try testing.expectEqualStrings(
"Gabcdefghijklmnopqrstuvwxyz0123456789",
s.handler.buf[0..s.handler.len],
);
}
test "stream: apc scalar path matches" {
@@ -3917,3 +3932,29 @@ test "stream: apc scalar path matches" {
s.handler.buf[0..s.handler.len],
);
}
test "stream: apc vector boundaries match scalar path" {
const positions = [_]usize{ 15, 16, 17, 31, 32, 33, 63, 64, 65 };
const controls = [_]u8{ 0x18, 0x1A, 0x1B, 0x80, 0xFF };
for (positions) |position| for (controls) |control| {
var input: [96]u8 = undefined;
input[0..3].* = "\x1b_G".*;
@memset(input[3 .. 3 + position], 'a');
input[3 + position] = control;
input[4 + position] = '\\';
const bytes = input[0 .. 5 + position];
var bulk: Stream(ApcTestHandler) = .init(.{});
bulk.nextSlice(bytes);
var scalar: Stream(ApcTestHandler) = .init(.{});
for (bytes) |byte| scalar.next(byte);
try testing.expectEqual(scalar.handler.started, bulk.handler.started);
try testing.expectEqual(scalar.handler.ended, bulk.handler.ended);
try testing.expectEqualStrings(
scalar.handler.buf[0..scalar.handler.len],
bulk.handler.buf[0..bulk.handler.len],
);
};
}