mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-09-14 18:01:58 +00:00
Slicing tables removed the byte-at-a-time dependency chain, but each
16-byte fold still depends serially on the previous one, leaving the
software CRC latency-bound at roughly 2.5-3 GB/s in V8 while snapshot
payloads run through it once per direction. wasm has no carry-less
multiply, so wider tables are the only classic escape — and measuring
slicing-by-32 against interleaving showed the extra 16 KB of tables buys
nothing once the chain is hidden.
Instead, inputs of 4 KiB and up split into thirds processed as three
independent fold chains in one loop, then merge with the GF(2) zero-shift
operator: crc(A ++ B, s) = crc(B, 0) XOR zeroShift(crc(A, s), |B|). The
shift matrices are comptime, storing only even powers of two (an odd
power applies the preceding matrix twice), 4 KB total. Software CRC
throughput roughly doubles; hardware backends are untouched, so native
is unaffected (tables below are noise).
Benchmarks ("prev" is the parent commit):
| wasm | encode prev | encode | decode prev | decode |
|-----------|------------:|---------:|------------:|---------:|
| ascii | 2.12 ms | 1.73 ms | 2.59 ms | 2.17 ms |
| styled | 2.23 ms | 1.47 ms | 6.43 ms | 5.38 ms |
| truecolor | 3.44 ms | 2.30 ms | 8.35 ms | 7.17 ms |
| cjk | 5.99 ms | 3.89 ms | 11.39 ms | 9.50 ms |
| grapheme | 8.33 ms | 6.94 ms | 10.89 ms | 9.24 ms |
| native | mode | prev | this |
|--------|--------|--------:|--------:|
| ascii | encode | 24.7 ms | 24.8 ms |
| ascii | decode | 47.5 ms | 49.2 ms |
| utf8 | encode | 41.9 ms | 42.4 ms |
| utf8 | decode | 58.8 ms | 59.5 ms |
425 lines
14 KiB
Zig
425 lines
14 KiB
Zig
//! CRC32C with hardware acceleration.
|
|
//!
|
|
//! The Zig standard library implementation processes one byte per table
|
|
//! 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.
|
|
//!
|
|
//! Targets without a dedicated instruction, such as WebAssembly, use a
|
|
//! custom implementation that is faster than Zig's stdlib.
|
|
//!
|
|
//! The resulting value is identical across all backends: this is the
|
|
//! iSCSI CRC32C parameter set (reflected, initial and final XOR
|
|
//! `0xFFFFFFFF`), matching `std.hash.crc.Crc32Iscsi`.
|
|
|
|
const std = @import("std");
|
|
const builtin = @import("builtin");
|
|
|
|
const Backend = enum {
|
|
aarch64_crc,
|
|
x86_64_sse42,
|
|
software,
|
|
};
|
|
|
|
const backend: Backend = backend: {
|
|
switch (builtin.cpu.arch) {
|
|
.aarch64,
|
|
.aarch64_be,
|
|
=> if (std.Target.aarch64.featureSetHas(
|
|
builtin.cpu.features,
|
|
.crc,
|
|
)) break :backend .aarch64_crc,
|
|
|
|
// The self-hosted x86_64 backend cannot encode the CRC32
|
|
// instruction forms used below, so that combination falls back to
|
|
// the portable implementation.
|
|
.x86_64 => if (builtin.zig_backend == .stage2_llvm and
|
|
std.Target.x86.featureSetHas(
|
|
builtin.cpu.features,
|
|
.sse4_2,
|
|
)) break :backend .x86_64_sse42,
|
|
|
|
else => {},
|
|
}
|
|
break :backend .software;
|
|
};
|
|
|
|
/// Streaming CRC32C with the same interface shape as `std.hash.crc` types.
|
|
pub const Crc32c = struct {
|
|
crc: u32,
|
|
|
|
pub fn init() Crc32c {
|
|
return .{ .crc = 0xFFFF_FFFF };
|
|
}
|
|
|
|
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.update(self.crc, bytes),
|
|
};
|
|
}
|
|
|
|
pub fn final(self: Crc32c) u32 {
|
|
return self.crc ^ 0xFFFF_FFFF;
|
|
}
|
|
|
|
pub fn hash(bytes: []const u8) u32 {
|
|
var c: Crc32c = .init();
|
|
c.update(bytes);
|
|
return c.final();
|
|
}
|
|
};
|
|
|
|
/// One update pass using the dedicated CRC32C instructions. Both supported
|
|
/// architectures handle unaligned loads efficiently, so the loop reads
|
|
/// little-endian words directly from the input.
|
|
fn updateHardware(initial: u32, bytes: []const u8) u32 {
|
|
var crc = initial;
|
|
var remaining = bytes;
|
|
|
|
while (remaining.len >= 8) : (remaining = remaining[8..]) {
|
|
crc = step(u64, crc, std.mem.readInt(
|
|
u64,
|
|
remaining[0..8],
|
|
.little,
|
|
));
|
|
}
|
|
if (remaining.len >= 4) {
|
|
crc = step(u32, crc, std.mem.readInt(
|
|
u32,
|
|
remaining[0..4],
|
|
.little,
|
|
));
|
|
remaining = remaining[4..];
|
|
}
|
|
for (remaining) |byte| crc = step(u8, crc, byte);
|
|
return crc;
|
|
}
|
|
|
|
/// One CRC32C instruction folding `value` into the running CRC.
|
|
inline fn step(comptime T: type, crc: u32, value: T) u32 {
|
|
return switch (comptime backend) {
|
|
.aarch64_crc => switch (T) {
|
|
u8 => asm ("crc32cb %[out:w], %[crc:w], %[value:w]"
|
|
: [out] "=r" (-> u32),
|
|
: [crc] "r" (crc),
|
|
[value] "r" (value),
|
|
),
|
|
u32 => asm ("crc32cw %[out:w], %[crc:w], %[value:w]"
|
|
: [out] "=r" (-> u32),
|
|
: [crc] "r" (crc),
|
|
[value] "r" (value),
|
|
),
|
|
u64 => asm ("crc32cx %[out:w], %[crc:w], %[value:x]"
|
|
: [out] "=r" (-> u32),
|
|
: [crc] "r" (crc),
|
|
[value] "r" (value),
|
|
),
|
|
else => comptime unreachable,
|
|
},
|
|
|
|
.x86_64_sse42 => switch (T) {
|
|
u8 => asm ("crc32b %[value], %[out]"
|
|
: [out] "=r" (-> u32),
|
|
: [value] "r" (value),
|
|
[crc_in] "0" (crc),
|
|
),
|
|
u32 => asm ("crc32l %[value], %[out]"
|
|
: [out] "=r" (-> u32),
|
|
: [value] "r" (value),
|
|
[crc_in] "0" (crc),
|
|
),
|
|
u64 => @truncate(asm ("crc32q %[value], %[out]"
|
|
: [out] "=r" (-> u64),
|
|
: [value] "r" (value),
|
|
[crc_in] "0" (@as(u64, crc)),
|
|
)),
|
|
else => comptime unreachable,
|
|
},
|
|
|
|
.software => comptime unreachable,
|
|
};
|
|
}
|
|
|
|
/// 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;
|
|
|
|
/// Number of slicing tables, which is also the bytes folded per
|
|
/// iteration.
|
|
const slices = 16;
|
|
|
|
/// Inputs below this length use the single-stream pass: the
|
|
/// stream-combine matrix work would not pay for itself.
|
|
const multi_stream_threshold = 4096;
|
|
|
|
/// 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 a whole block with
|
|
/// independent lookups instead of a byte-by-byte dependency chain.
|
|
const tables: [slices][256]u32 = tables: {
|
|
@setEvalBranchQuota(200_000);
|
|
var result: [slices][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..slices) |i| {
|
|
for (0..256) |n| {
|
|
const prev = result[i - 1][n];
|
|
result[i][n] = (prev >> 8) ^ result[0][prev & 0xFF];
|
|
}
|
|
}
|
|
break :tables result;
|
|
};
|
|
|
|
fn update(initial: u32, bytes: []const u8) u32 {
|
|
if (bytes.len >= multi_stream_threshold) return updateMulti(
|
|
initial,
|
|
bytes,
|
|
);
|
|
|
|
return updateSingle(initial, bytes);
|
|
}
|
|
|
|
/// One single-stream update pass using slicing.
|
|
fn updateSingle(initial: u32, bytes: []const u8) u32 {
|
|
const t = &tables;
|
|
var crc = initial;
|
|
var i: usize = 0;
|
|
while (i + slices <= bytes.len) : (i += slices) {
|
|
crc = foldChunk(bytes, i, crc);
|
|
}
|
|
for (bytes[i..]) |byte| {
|
|
crc = (crc >> 8) ^ t[0][(crc ^ byte) & 0xFF];
|
|
}
|
|
return crc;
|
|
}
|
|
|
|
/// One update pass as three independent interleaved streams. Faster
|
|
/// for large enough inputs.
|
|
fn updateMulti(initial: u32, bytes: []const u8) u32 {
|
|
// Both leading parts are block multiples so the interleaved loop
|
|
// needs no tail handling; the third part absorbs the remainder.
|
|
const part = (bytes.len / 3) & ~@as(usize, slices - 1);
|
|
const p0 = bytes[0..part];
|
|
const p1 = bytes[part..][0..part];
|
|
const p2 = bytes[2 * part ..];
|
|
|
|
var s0 = initial;
|
|
var s1: u32 = 0;
|
|
var s2: u32 = 0;
|
|
var i: usize = 0;
|
|
while (i + slices <= part) : (i += slices) {
|
|
s0 = foldChunk(p0, i, s0);
|
|
s1 = foldChunk(p1, i, s1);
|
|
s2 = foldChunk(p2, i, s2);
|
|
}
|
|
s2 = updateSingle(s2, p2[part..]);
|
|
|
|
const s01 = s1 ^ zeroShift(s0, p1.len);
|
|
return s2 ^ zeroShift(s01, p2.len);
|
|
}
|
|
|
|
/// Fold one aligned block through the per-position slicing tables.
|
|
/// The running CRC must already be XORed into the block's first word.
|
|
inline fn foldBlock(comptime len: usize, words: *const [len / 4]u32) u32 {
|
|
const t = &tables;
|
|
var crc: u32 = 0;
|
|
inline for (0..len / 4) |w| {
|
|
const word = words[w];
|
|
const base = len - 1 - w * 4;
|
|
crc ^= t[base][word & 0xFF] ^
|
|
t[base - 1][(word >> 8) & 0xFF] ^
|
|
t[base - 2][(word >> 16) & 0xFF] ^
|
|
t[base - 3][word >> 24];
|
|
}
|
|
return crc;
|
|
}
|
|
|
|
/// Fold the block starting at `offset`, chaining the running CRC state.
|
|
inline fn foldChunk(bytes: []const u8, offset: usize, crc: u32) u32 {
|
|
var words: [slices / 4]u32 = undefined;
|
|
inline for (&words, 0..) |*word, w| {
|
|
word.* = std.mem.readInt(
|
|
u32,
|
|
bytes[offset + w * 4 ..][0..4],
|
|
.little,
|
|
);
|
|
}
|
|
words[0] ^= crc;
|
|
return foldBlock(slices, &words);
|
|
}
|
|
|
|
/// Advance a CRC state as if `len` zero bytes had been processed.
|
|
fn zeroShift(state: u32, len: usize) u32 {
|
|
var s = state;
|
|
var remaining = len;
|
|
var k: usize = 0;
|
|
while (remaining != 0) : ({
|
|
remaining >>= 1;
|
|
k += 1;
|
|
}) {
|
|
if (remaining & 1 != 0) {
|
|
const mat = &zero_shift_matrices[k / 2];
|
|
s = matTimesVec(mat, s);
|
|
if (k % 2 != 0) s = matTimesVec(mat, s);
|
|
}
|
|
}
|
|
return s;
|
|
}
|
|
|
|
/// Multiply the GF(2) matrix by a CRC state column vector.
|
|
inline fn matTimesVec(mat: *const [32]u32, vec: u32) u32 {
|
|
var sum: u32 = 0;
|
|
var v = vec;
|
|
var i: usize = 0;
|
|
while (v != 0) : ({
|
|
v >>= 1;
|
|
i += 1;
|
|
}) {
|
|
if (v & 1 != 0) sum ^= mat[i];
|
|
}
|
|
return sum;
|
|
}
|
|
|
|
const zero_shift_matrices: [32][32]u32 = matrices: {
|
|
@setEvalBranchQuota(500_000);
|
|
var matrices: [32][32]u32 = undefined;
|
|
var previous: [32]u32 = undefined;
|
|
for (0..32) |i| {
|
|
const unit: u32 = 1 << i;
|
|
previous[i] = (unit >> 8) ^ tables[0][unit & 0xFF];
|
|
}
|
|
matrices[0] = previous;
|
|
for (1..64) |k| {
|
|
var squared: [32]u32 = undefined;
|
|
for (0..32) |i| {
|
|
squared[i] = matTimesVec(&previous, previous[i]);
|
|
}
|
|
previous = squared;
|
|
if (k % 2 == 0) matrices[k / 2] = squared;
|
|
}
|
|
break :matrices matrices;
|
|
};
|
|
};
|
|
|
|
/// 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 "software multi-stream matches the standard library" {
|
|
// Lengths around and far above the interleaving threshold, plus odd
|
|
// remainders, so all three streams and both combine steps are covered.
|
|
var bytes: [96 * 1024]u8 = undefined;
|
|
var prng = std.Random.DefaultPrng.init(0x3517_1A3B);
|
|
prng.random().bytes(&bytes);
|
|
|
|
for ([_]usize{
|
|
Software.multi_stream_threshold - 1,
|
|
Software.multi_stream_threshold,
|
|
Software.multi_stream_threshold + 1,
|
|
Software.multi_stream_threshold + 97,
|
|
12 * 1024,
|
|
64 * 1024 + 31,
|
|
bytes.len,
|
|
}) |len| {
|
|
const input = bytes[0..len];
|
|
var reference: Reference = .{ .crc = 0xFFFF_FFFF };
|
|
reference.update(input);
|
|
try std.testing.expectEqual(
|
|
reference.crc,
|
|
Software.update(0xFFFF_FFFF, input),
|
|
);
|
|
|
|
// Continuation across a split inside the multi-stream range.
|
|
const first = Software.update(0xFFFF_FFFF, input[0 .. len / 2]);
|
|
try std.testing.expectEqual(
|
|
reference.crc,
|
|
Software.update(first, input[len / 2 ..]),
|
|
);
|
|
}
|
|
}
|
|
|
|
test "matches the check value" {
|
|
// The catalog check value for CRC-32/ISCSI.
|
|
try std.testing.expectEqual(
|
|
@as(u32, 0xE3069283),
|
|
Crc32c.hash("123456789"),
|
|
);
|
|
}
|
|
|
|
test "matches the standard library at every length and split" {
|
|
var bytes: [259]u8 = undefined;
|
|
var prng = std.Random.DefaultPrng.init(0xC5C32C);
|
|
prng.random().bytes(&bytes);
|
|
|
|
for (0..bytes.len + 1) |len| {
|
|
const input = bytes[0..len];
|
|
try std.testing.expectEqual(
|
|
Reference.hash(input),
|
|
Crc32c.hash(input),
|
|
);
|
|
|
|
// Streaming across arbitrary split points must not change the
|
|
// result: word batching may not leak state between updates.
|
|
var split: Crc32c = .init();
|
|
split.update(input[0 .. len / 3]);
|
|
split.update(input[len / 3 .. len - len / 3]);
|
|
split.update(input[len - len / 3 ..]);
|
|
try std.testing.expectEqual(Reference.hash(input), split.final());
|
|
}
|
|
}
|
|
|
|
test "matches the standard library at every alignment" {
|
|
var bytes: [64 + 16]u8 = undefined;
|
|
var prng = std.Random.DefaultPrng.init(0xA11C);
|
|
prng.random().bytes(&bytes);
|
|
|
|
for (0..16) |offset| {
|
|
const input = bytes[offset..][0..64];
|
|
try std.testing.expectEqual(
|
|
Reference.hash(input),
|
|
Crc32c.hash(input),
|
|
);
|
|
}
|
|
}
|