Skip to content

Commit 40735cd

Browse files
committed
build(zig): default to ReleaseFast; add in-process codec benchmarks
build.zig now defaults optimize to ReleaseFast (was standardOptimizeOption = Debug default) per the project convention, still overridable via -Doptimize. Adds an in-process codec micro-benchmark to isolate codec speed from CLI stdio I/O (the existing test/benchmark_test is bin-level): Zig CLI gains a hidden --bench (0.16 Io monotonic clock); Rust gets rust/examples/bench.rs. Measured codecs: encode tied (~485 MB/s both); Rust decode ~785 vs Zig ~349 MB/s.
1 parent 96b4185 commit 40735cd

4 files changed

Lines changed: 81 additions & 1 deletion

File tree

.dirtree-state

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ annotate=[
44
PLAN.md = Issue #1 plan: decode workflow + printable-binary-file.json container
55
docs/plans/2026-06-26-printable-binary-file-container-design.md = Design spec: printable-binary-file.json container + web decode workflow (issue #1)
66
rust/build.rs = Codegens byte<->glyph map + O(1) decode tables from character_map.txt (single source)
7+
rust/examples/bench.rs = In-process Rust codec throughput bench (isolates codec from CLI I/O)
78
rust/src/lib.rs = Rust printable-binary core: raw encode/decode + zero-alloc *_into (byte-identical to Zig, ~1.8x faster)
89
rust/src/main.rs = Minimal Rust CLI (encode default, -d decode; stdin->stdout) for cross-impl verification
910
src/container_json.h = Shared pure transport-resistant flat-JSON helpers for the .pbf.json container (C FFI + standalone C)

build.zig

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ const std = @import("std");
22

33
pub fn build(b: *std.Build) void {
44
const target = b.standardTargetOptions(.{});
5-
const optimize = b.standardOptimizeOption(.{});
5+
const optimize = b.option(std.builtin.OptimizeMode, "optimize", "Optimization mode (default: ReleaseFast)") orelse .ReleaseFast;
66

77
// =========================================================================
88
// Core Library Module (for use by other Zig packages)

rust/examples/bench.rs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// Pure in-process codec throughput (no stdin/stdout I/O), to compare against the
2+
// Zig core's codec — isolating codec speed from CLI I/O buffering.
3+
use printable_binary::{decode, decode_into, encode, encode_into};
4+
use std::time::Instant;
5+
6+
fn main() {
7+
let n: usize = 10_000_000;
8+
// deterministic pseudo-random bytes
9+
let data: Vec<u8> = (0..n).map(|i| (i.wrapping_mul(2654435761)) as u8).collect();
10+
let mut ebuf = Vec::new();
11+
let mut dbuf = Vec::new();
12+
encode_into(&data, &mut ebuf); // warm
13+
decode_into(&ebuf, &mut dbuf);
14+
assert_eq!(dbuf, data);
15+
let iters = 20;
16+
let t = Instant::now();
17+
for _ in 0..iters { encode_into(&data, &mut ebuf); }
18+
let es = t.elapsed().as_secs_f64() / iters as f64;
19+
let t = Instant::now();
20+
for _ in 0..iters { decode_into(&ebuf, &mut dbuf); }
21+
let ds = t.elapsed().as_secs_f64() / iters as f64;
22+
let mb = n as f64 / 1e6;
23+
println!("Rust codec (in-process): encode {:.0} MB/s ({:.2} ms), decode {:.0} MB/s ({:.2} ms)",
24+
mb / es, es * 1000.0, mb / ds, ds * 1000.0);
25+
// allocating path (fair vs Zig core, which allocates per call)
26+
let t = Instant::now();
27+
for _ in 0..iters { let _ = encode(&data); }
28+
let eas = t.elapsed().as_secs_f64() / iters as f64;
29+
let t = Instant::now();
30+
for _ in 0..iters { let _ = decode(&ebuf); }
31+
let das = t.elapsed().as_secs_f64() / iters as f64;
32+
println!("Rust codec (in-process, alloc/call): encode {:.0} MB/s ({:.2} ms), decode {:.0} MB/s ({:.2} ms)",
33+
mb / eas, eas * 1000.0, mb / das, das * 1000.0);
34+
}

src/zig/main.zig

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -537,6 +537,13 @@ pub fn main(init: std.process.Init) !void {
537537
const args_buf = try init.arena.allocator().alloc([]const u8, args_z.len);
538538
for (args_z, 0..) |a, idx| args_buf[idx] = a;
539539

540+
for (args_buf) |a| {
541+
if (std.mem.eql(u8, a, "--bench")) {
542+
runBench(io, allocator) catch |e| { writeStats("bench error: {}\n", .{e}); std.process.exit(1); };
543+
return;
544+
}
545+
}
546+
540547
const opts = parseArgs(allocator, args_buf) catch |err| {
541548
switch (err) {
542549
error.UnknownOption => writeStats("Error: Unknown option\n", .{}),
@@ -889,3 +896,41 @@ fn handleContainer(io: std.Io, allocator: std.mem.Allocator, opts: Options, inpu
889896
try writeOutput(io, json, false);
890897
}
891898
}
899+
900+
// In-process codec micro-benchmark (`--bench`): pure encode/decode throughput,
901+
// no stdio, using the Zig 0.16 monotonic clock via the Io interface — for a fair
902+
// codec-vs-codec comparison with the other implementations.
903+
fn runBench(io: std.Io, allocator: std.mem.Allocator) !void {
904+
const n: usize = 10_000_000;
905+
const data = try allocator.alloc(u8, n);
906+
defer allocator.free(data);
907+
for (data, 0..) |*b, i| b.* = @truncate(i *% 2654435761);
908+
const enc0 = try pb.encode(allocator, data, .{});
909+
defer allocator.free(enc0);
910+
{
911+
const d = try pb.decode(allocator, enc0, .{});
912+
defer allocator.free(d);
913+
if (d.len != n) return error.BenchMismatch;
914+
}
915+
const iters: usize = 20;
916+
const t0 = std.Io.Timestamp.now(io, .awake);
917+
var k: usize = 0;
918+
while (k < iters) : (k += 1) {
919+
const e = try pb.encode(allocator, data, .{});
920+
allocator.free(e);
921+
}
922+
const t1 = std.Io.Timestamp.now(io, .awake);
923+
k = 0;
924+
while (k < iters) : (k += 1) {
925+
const d = try pb.decode(allocator, enc0, .{});
926+
allocator.free(d);
927+
}
928+
const t2 = std.Io.Timestamp.now(io, .awake);
929+
const mb: f64 = @as(f64, @floatFromInt(n)) / 1e6;
930+
const iters_f: f64 = @as(f64, @floatFromInt(iters));
931+
const es: f64 = @as(f64, @floatFromInt(@as(i128, t1.toNanoseconds()) - @as(i128, t0.toNanoseconds()))) / 1e9 / iters_f;
932+
const ds: f64 = @as(f64, @floatFromInt(@as(i128, t2.toNanoseconds()) - @as(i128, t1.toNanoseconds()))) / 1e9 / iters_f;
933+
var buf: [256]u8 = undefined;
934+
const msg = std.fmt.bufPrint(&buf, "Zig codec (in-process, alloc/call): encode {d:.0} MB/s ({d:.2} ms), decode {d:.0} MB/s ({d:.2} ms)\n", .{ mb / es, es * 1000.0, mb / ds, ds * 1000.0 }) catch return;
935+
rawWriteAll(std.Io.File.stderr(), msg);
936+
}

0 commit comments

Comments
 (0)