Skip to content

Commit f330f06

Browse files
committed
perf(zig): vectorize literal decode prefix
1 parent 7374ed0 commit f330f06

4 files changed

Lines changed: 20 additions & 5 deletions

File tree

.dirtree-state

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ annotate=[
1515
rust/src/main.rs = Minimal Rust CLI (encode default, -d decode; stdin->stdout) for cross-impl verification
1616
src/container_json.h = Shared pure transport-resistant flat-JSON helpers for the .pbf.json container (C FFI + standalone C)
1717
src/zig/ffi.zig = C ABI (FFI) export surface: all 12 pb_* C functions; root of libprintable_binary.a; keeps C symbols OUT of the importable printable_binary module so static (musl) consumers don't collide
18-
src/zig/printable_binary.zig = Pure Zig codec core; default encode combines a portable 16-byte SIMD literal-prefix gate with padded glyph slots, and decode uses compact direct UTF-8 tables.
18+
src/zig/printable_binary.zig = Pure Zig codec core; default encode/decode use a portable 16-byte SIMD literal-prefix gate, padded encode slots, and compact direct UTF-8 decode tables.
1919
test/module_consumer.zig = Test fixture: minimal downstream importer of the printable_binary Zig module (mirrors how difz/blip consume it) for the FFI-symbol-leak test
2020
test/test_container = Container (.pbf.json) CLI round-trip + self-verify test, parameterized by IMPLEMENTATION_TO_TEST (issue #1)
2121
test/test_container_cross = Cross-impl container differential: impl-A container decodes via impl-B (MFIC, issue #1)

PLAN.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,10 @@ maintained_by: agent
4949
on simdutf's block classification rather than depending on it. LLVM emits
5050
a `<16 x i8>` load and vector range checks. Mixed encode remains
5151
~612–629 MB/s; a 10 MB all-literal CLI input improved from 46.2 to
52-
37.1 ms (~20%). Curiosity poke: this gate stops at the first mapped glyph,
53-
so prose containing default-encoded spaces needs a separate benchmarked
54-
design. (2026-07-23 12:28 AM EDT)
52+
37.1 ms (~20%) on encode and from 41.6 to 25.1 ms (~40%) on decode.
53+
Curiosity poke: this gate stops at the first mapped glyph, so prose
54+
containing default-encoded spaces needs a separate benchmarked design.
55+
(2026-07-23 12:35 AM EDT)
5556

5657
## Container honors `--spaces` (legible-markdown containers) — DONE (2026-07-21 EDT)
5758

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ The Rust crate also has an in-process codec microbenchmark (`nix develop -c carg
7777
Key optimizations in the Zig core:
7878
- **Pre-allocated buffers**: encode/decode output sized upfront (no growth checks in the hot loop).
7979
- **Flat character map**: a comptime-built contiguous byte buffer (~1.5 KB) replacing 256 scattered fat pointers — fits in L1 cache.
80-
- **SIMD literal-prefix gate**: a portable 16-byte vector check copies a contiguous run of literal passthrough glyphs unchanged, then falls back to the compact variable-width mapper at the first mapped byte.
80+
- **SIMD literal-prefix gate**: a portable 16-byte vector check copies a contiguous run of literal passthrough glyphs unchanged during both encode and decode, then falls back to the compact variable-width mapper at the first mapped byte.
8181
- **O(1) decode lookup**: direct tables for 1- and 2-byte UTF-8 sequences plus a compact 24 KiB table for the map's three 3-byte lead-byte planes, replacing the former binary search.
8282
- **No inner decode loop**: a single UTF-8 length check + direct lookup per character.
8383

src/zig/printable_binary.zig

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -466,6 +466,12 @@ fn decodeDefault(allocator: std.mem.Allocator, input: []const u8) ![]u8 {
466466

467467
var i: usize = 0;
468468
var pos: usize = 0;
469+
while (input.len - i >= 16 and isSelfMappedBlock16(input[i..][0..16])) {
470+
@memcpy(result[pos..][0..16], input[i..][0..16]);
471+
i += 16;
472+
pos += 16;
473+
}
474+
469475
while (i < input.len) {
470476
const seq_len = utf8SeqLen(input[i]);
471477
const remaining = input.len - i;
@@ -1197,6 +1203,14 @@ test "decode: unrecognized UTF-8 passes through" {
11971203
try std.testing.expectEqualSlices(u8, input, decoded);
11981204
}
11991205

1206+
test "decode: 16-byte literal prefix reaches the following mapped glyph" {
1207+
const allocator = std.testing.allocator;
1208+
const input = "ABCDEFGHIJKLMNOP" ++ character_map[0];
1209+
const decoded = try decode(allocator, input, .{});
1210+
defer allocator.free(decoded);
1211+
try std.testing.expectEqualStrings("ABCDEFGHIJKLMNOP\x00", decoded);
1212+
}
1213+
12001214
test "decode: mixed known and unknown UTF-8" {
12011215
const allocator = std.testing.allocator;
12021216
// "Hello" in PB + an unknown character + "World" in PB

0 commit comments

Comments
 (0)