Skip to content

Commit 7374ed0

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

4 files changed

Lines changed: 40 additions & 8 deletions

File tree

.dirtree-state

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ ver=1.2
33
annotate=[
44
.mechatron-prime/targets = Exact Nix package and check targets that Mechatron Prime builds from each pushed commit.
55
MEMORIES/WASM benchmarks must stream inputs and APE needs a clean environment = WASI and Cosmopolitan runtime requirements for benchmark adapters.
6-
PLAN.md = Active project plan; records benchmarked default-path optimization and remaining SIMD/decode experiments.
6+
PLAN.md = Active project plan; records measured Zig codec performance wins, a rejected scalar ASCII experiment, and remaining API-design questions.
77
bm/benchmark-zig-opt = Benchmarks all available CLI variants, including Rust stdin, WASM, and APE adapters.
88
docs/plans/2026-06-26-printable-binary-file-container-design.md = Design spec: printable-binary-file.json container + web decode workflow (issue #1)
99
elixir/lib/printable_binary.ex = Elixir ~PB compile-time sigil: decodes printable-binary glyphs to raw bytes at compile time (reads character_map.txt via @external_resource); decode/1 runtime helper (whitespace-tolerant)
@@ -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 uses four-byte padded slots and decode uses compact direct UTF-8 lookup tables.
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.
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: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,11 +45,13 @@ maintained_by: agent
4545
classifier branch. Curiosity poke: revisit only with a genuinely vector
4646
block classifier that does not tax non-passthrough data. (2026-07-23
4747
12:25 AM EDT)
48-
- [ ] Prototype a portable SIMD/hybrid classifier only if the measured
49-
scalar loop remains dominant. Use `simdutf` as a technique reference,
50-
not a dependency: its UTF-8 transcoder cannot directly express this
51-
custom byte-to-glyph map. Curiosity poke: 1–3-byte output compaction can
52-
cost more than the scalar table path on mixed data.
48+
- [x] Add a portable 16-byte SIMD literal-prefix gate using `@Vector`, modeled
49+
on simdutf's block classification rather than depending on it. LLVM emits
50+
a `<16 x i8>` load and vector range checks. Mixed encode remains
51+
~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)
5355

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

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +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.
8081
- **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.
8182
- **No inner decode loop**: a single UTF-8 length check + direct lookup per character.
8283

src/zig/printable_binary.zig

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,20 @@ const padded_map_data: [256][4]u8 = blk: {
112112
break :blk data;
113113
};
114114

115+
/// Check one vector-width block against the map's literal ASCII passthrough
116+
/// ranges. This is the simdutf-style common-case gate: successful blocks copy
117+
/// unchanged, while mixed/binary input immediately falls back to scalar slots.
118+
fn isSelfMappedBlock16(bytes: *const [16]u8) bool {
119+
const ByteVector = @Vector(16, u8);
120+
const block: ByteVector = bytes.*;
121+
const period = block == @as(ByteVector, @splat('.'));
122+
const digits = (block >= @as(ByteVector, @splat('0'))) & (block <= @as(ByteVector, @splat('9')));
123+
const upper = (block >= @as(ByteVector, @splat('A'))) & (block <= @as(ByteVector, @splat('Z')));
124+
const punctuation = (block >= @as(ByteVector, @splat('^'))) & (block <= @as(ByteVector, @splat('_')));
125+
const lower = (block >= @as(ByteVector, @splat('a'))) & (block <= @as(ByteVector, @splat('z')));
126+
return @reduce(.And, period | digits | upper | punctuation | lower);
127+
}
128+
115129
/// Direct O(1) decode lookup for 1-byte UTF-8 sequences
116130
const decode_1byte: [256]?u8 = blk: {
117131
@setEvalBranchQuota(100000);
@@ -379,8 +393,15 @@ fn encodeDefault(allocator: std.mem.Allocator, input: []const u8) ![]u8 {
379393
var result = try allocator.alloc(u8, capacity);
380394
errdefer allocator.free(result);
381395

396+
var input_pos: usize = 0;
382397
var pos: usize = 0;
383-
for (input) |byte| {
398+
while (input.len - input_pos >= 16 and isSelfMappedBlock16(input[input_pos..][0..16])) {
399+
@memcpy(result[pos..][0..16], input[input_pos..][0..16]);
400+
input_pos += 16;
401+
pos += 16;
402+
}
403+
404+
for (input[input_pos..]) |byte| {
384405
@memcpy(result[pos..][0..4], &padded_map_data[byte]);
385406
pos += flat_map_entries[byte].len;
386407
}
@@ -1145,6 +1166,14 @@ test "encode: ASCII passthrough characters are preserved" {
11451166
try std.testing.expectEqualStrings("Hello.World@123", encoded);
11461167
}
11471168

1169+
test "encode: 16-byte literal prefix remains compact before a mapped glyph" {
1170+
const allocator = std.testing.allocator;
1171+
const input = "ABCDEFGHIJKLMNOP\x00";
1172+
const encoded = try encode(allocator, input, .{});
1173+
defer allocator.free(encoded);
1174+
try std.testing.expectEqualStrings("ABCDEFGHIJKLMNOP" ++ character_map[0], encoded);
1175+
}
1176+
11481177
test "encode: spaces option preserves literal spaces" {
11491178
const allocator = std.testing.allocator;
11501179
const input = "A B";

0 commit comments

Comments
 (0)