mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-08-24 16:11:43 +00:00
terminal/snapshot: slicing-by-16 software CRC32C
The software CRC32C fallback (WebAssembly and any other target without a dedicated instruction) was the std byte-at-a-time table walk, which profiled at ~65% of snapshot encode and ~70% of decode self-time in V8. Replace it with slicing-by-16: sixteen bytes fold per iteration through comptime per-position tables, so the serial dependency advances one block at a time instead of one byte. The hardware backends (aarch64 CRC, x86_64 SSE4.2) are unchanged, so native is expected to be unaffected; its deltas below are run-to-run noise. Benchmarks: wasm is V8 (node 25), ReleaseFast + wasm-opt -O3, 80x24 terminal, 2 MiB VT corpus per workload, complete snapshot including scrollback, best-of-5. Native is aarch64 macOS, hyperfine mean, ghostty-bench +terminal-snapshot --loops=20. "base" is the parent commit. | wasm | encode base | encode | decode base | decode | |-----------|------------:|---------:|------------:|---------:| | ascii | 7.07 ms | 3.43 ms | 6.45 ms | 2.81 ms | | styled | 10.54 ms | 3.14 ms | 14.37 ms | 7.12 ms | | truecolor | 15.23 ms | 5.33 ms | 21.76 ms | 12.11 ms | | cjk | 25.41 ms | 5.95 ms | 30.83 ms | 12.04 ms | | grapheme | 27.67 ms | 14.18 ms | 27.00 ms | 13.16 ms | | native | mode | base | this | |--------|--------|--------:|--------:| | ascii | encode | 40.6 ms | 41.7 ms | | ascii | decode | 51.2 ms | 53.2 ms | | utf8 | encode | 45.2 ms | 47.2 ms | | utf8 | decode | 59.8 ms | 61.6 ms |
This commit is contained in:
118
src/crc32c.zig
118
src/crc32c.zig
@@ -4,8 +4,11 @@
|
||||
//! lookup (as of Zig 0.16), which is more than an order of magnitude slower
|
||||
//! than the dedicated CRC32C instructions available on aarch64 (CRC
|
||||
//! extension) and x86_64 (SSE4.2). This module selects the best backend at
|
||||
//! compile time and falls back to the standard library elsewhere, including
|
||||
//! WebAssembly.
|
||||
//! compile time.
|
||||
//!
|
||||
//! Targets without a dedicated instruction, such as WebAssembly, use a
|
||||
//! slicing-by-16 table implementation that processes sixteen bytes per
|
||||
//! iteration instead of one.
|
||||
//!
|
||||
//! The resulting value is identical across all backends: this is the
|
||||
//! iSCSI CRC32C parameter set (reflected, initial and final XOR
|
||||
@@ -14,10 +17,6 @@
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
|
||||
/// The standard-library implementation of the same parameter set. This is
|
||||
/// both the portable fallback and the reference the tests compare against.
|
||||
const Software = std.hash.crc.Crc32Iscsi;
|
||||
|
||||
const Backend = enum {
|
||||
aarch64_crc,
|
||||
x86_64_sse42,
|
||||
@@ -58,11 +57,7 @@ pub const Crc32c = struct {
|
||||
pub fn update(self: *Crc32c, bytes: []const u8) void {
|
||||
self.crc = switch (comptime backend) {
|
||||
.aarch64_crc, .x86_64_sse42 => updateHardware(self.crc, bytes),
|
||||
.software => software: {
|
||||
var crc: Software = .{ .crc = self.crc };
|
||||
crc.update(bytes);
|
||||
break :software crc.crc;
|
||||
},
|
||||
.software => Software.update(self.crc, bytes),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -148,6 +143,101 @@ inline fn step(comptime T: type, crc: u32, value: T) u32 {
|
||||
};
|
||||
}
|
||||
|
||||
/// The portable software backend, used by targets without a dedicated
|
||||
/// CRC32C instruction.
|
||||
const Software = struct {
|
||||
/// The reflected CRC32C (Castagnoli) polynomial.
|
||||
const reflected_poly: u32 = 0x82F63B78;
|
||||
|
||||
/// Slicing tables: `tables[i][b]` is the CRC of byte `b` followed
|
||||
/// by `i` zero bytes. Table zero is the classic one-byte-per-step table;
|
||||
/// the higher tables let one iteration fold sixteen input bytes with
|
||||
/// sixteen independent lookups instead of a sixteen-step dependency chain.
|
||||
const tables: [16][256]u32 = tables: {
|
||||
@setEvalBranchQuota(100_000);
|
||||
var result: [16][256]u32 = undefined;
|
||||
for (0..256) |n| {
|
||||
var crc: u32 = n;
|
||||
for (0..8) |_| {
|
||||
crc = (crc >> 1) ^ (reflected_poly * (crc & 1));
|
||||
}
|
||||
result[0][n] = crc;
|
||||
}
|
||||
for (1..16) |i| {
|
||||
for (0..256) |n| {
|
||||
const prev = result[i - 1][n];
|
||||
result[i][n] = (prev >> 8) ^ result[0][prev & 0xFF];
|
||||
}
|
||||
}
|
||||
break :tables result;
|
||||
};
|
||||
|
||||
/// One update pass using slicing-by-16: each iteration XORs the running
|
||||
/// CRC into the first of four little-endian words and folds all sixteen
|
||||
/// bytes through per-position tables. The remainder finishes one byte per
|
||||
/// step through table zero.
|
||||
fn update(initial: u32, bytes: []const u8) u32 {
|
||||
const t = &tables;
|
||||
var crc = initial;
|
||||
var remaining = bytes;
|
||||
|
||||
while (remaining.len >= 16) : (remaining = remaining[16..]) {
|
||||
const a = std.mem.readInt(u32, remaining[0..4], .little) ^ crc;
|
||||
const b = std.mem.readInt(u32, remaining[4..8], .little);
|
||||
const c = std.mem.readInt(u32, remaining[8..12], .little);
|
||||
const d = std.mem.readInt(u32, remaining[12..16], .little);
|
||||
crc = t[15][a & 0xFF] ^ t[14][(a >> 8) & 0xFF] ^
|
||||
t[13][(a >> 16) & 0xFF] ^ t[12][a >> 24] ^
|
||||
t[11][b & 0xFF] ^ t[10][(b >> 8) & 0xFF] ^
|
||||
t[9][(b >> 16) & 0xFF] ^ t[8][b >> 24] ^
|
||||
t[7][c & 0xFF] ^ t[6][(c >> 8) & 0xFF] ^
|
||||
t[5][(c >> 16) & 0xFF] ^ t[4][c >> 24] ^
|
||||
t[3][d & 0xFF] ^ t[2][(d >> 8) & 0xFF] ^
|
||||
t[1][(d >> 16) & 0xFF] ^ t[0][d >> 24];
|
||||
}
|
||||
for (remaining) |byte| {
|
||||
crc = (crc >> 8) ^ t[0][(crc ^ byte) & 0xFF];
|
||||
}
|
||||
return crc;
|
||||
}
|
||||
};
|
||||
|
||||
/// The standard-library implementation of the same parameter set. This is
|
||||
/// the reference the tests compare against.
|
||||
const Reference = std.hash.crc.Crc32Iscsi;
|
||||
|
||||
test "software slicing matches the standard library" {
|
||||
// The selected backend may be hardware, so cover the sliced software
|
||||
// path directly: every length around the sixteen-byte boundary, several
|
||||
// alignments, and continuation across arbitrary split points.
|
||||
var bytes: [512 + 19]u8 = undefined;
|
||||
var prng = std.Random.DefaultPrng.init(0x511C);
|
||||
prng.random().bytes(&bytes);
|
||||
|
||||
for (0..64 + 1) |len| {
|
||||
for (0..4) |offset| {
|
||||
const input = bytes[offset..][0..len];
|
||||
var reference: Reference = .{ .crc = 0xFFFF_FFFF };
|
||||
reference.update(input);
|
||||
try std.testing.expectEqual(
|
||||
reference.crc,
|
||||
Software.update(0xFFFF_FFFF, input),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const long = bytes[0..512];
|
||||
var reference: Reference = .{ .crc = 0xFFFF_FFFF };
|
||||
reference.update(long);
|
||||
for ([_]usize{ 0, 1, 15, 16, 17, 100, 511, 512 }) |split| {
|
||||
const first = Software.update(0xFFFF_FFFF, long[0..split]);
|
||||
try std.testing.expectEqual(
|
||||
reference.crc,
|
||||
Software.update(first, long[split..]),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
test "matches the check value" {
|
||||
// The catalog check value for CRC-32/ISCSI.
|
||||
try std.testing.expectEqual(
|
||||
@@ -164,7 +254,7 @@ test "matches the standard library at every length and split" {
|
||||
for (0..bytes.len + 1) |len| {
|
||||
const input = bytes[0..len];
|
||||
try std.testing.expectEqual(
|
||||
Software.hash(input),
|
||||
Reference.hash(input),
|
||||
Crc32c.hash(input),
|
||||
);
|
||||
|
||||
@@ -174,7 +264,7 @@ test "matches the standard library at every length and split" {
|
||||
split.update(input[0 .. len / 3]);
|
||||
split.update(input[len / 3 .. len - len / 3]);
|
||||
split.update(input[len - len / 3 ..]);
|
||||
try std.testing.expectEqual(Software.hash(input), split.final());
|
||||
try std.testing.expectEqual(Reference.hash(input), split.final());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -186,7 +276,7 @@ test "matches the standard library at every alignment" {
|
||||
for (0..16) |offset| {
|
||||
const input = bytes[offset..][0..64];
|
||||
try std.testing.expectEqual(
|
||||
Software.hash(input),
|
||||
Reference.hash(input),
|
||||
Crc32c.hash(input),
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user