Something went wrong. Try again.
A Zig library and command line tool for the classic Unix uuencode format.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us>// SPDX-License-Identifier: MIT
//! Benchmarks for the uuencode library, run with `zig build bench`.//!//! The question these answer is whether the vectorised kernels in `simd.zig`//! are worth having. Three implementations are timed side by side://!//! scalar what the library does today, options read at run time//! scalar+ct the same scalar code with the charset and line ending fixed//! at comptime, which is the control: it separates the gain that//! comes from specialising the loop from the gain that comes//! from the vector instructions//! simd the kernels in simd.zig, which are also comptime specialised//!//! Without that middle column a SIMD win is impossible to attribute, because//! the vector path specialises as well.//!//! Throughput is reported over payload bytes, so encode and decode figures at//! the same size describe the same amount of user data.
const std = @import("std");const Io = std.Io;
const zuucode = @import("zuucode");const uuencode = zuucode.uuencode;const simd = zuucode.simd;const uudecode = zuucode.uudecode;
const bytes_per_line = zuucode.bytes_per_line;
/// Roughly how much payload each measurement should push through, which sets/// the repetition count. Large enough to swamp timer and call overhead.const target_bytes: usize = 64 << 20;/// Measurements per configuration; the best is reported, as the fastest run/// is the one least disturbed by scheduling and interrupts.const trials = 5;
// ------------------------------------------------------- the scalar baselines//// zuucode.encode and zuucode.decode now take the vector path themselves, so a// scalar reference has to be assembled here out of encodeLineScalar and// decodeLineScalar. These mirror the library's own loops exactly.
fn encodeScalar(dest: []u8, source: []const u8, options: uuencode.Options) []u8 { const eol = options.line_ending.bytes(); var read: usize = 0; var written: usize = 0; while (read < source.len) : (read += bytes_per_line) { const chunk = source[read..@min(read + bytes_per_line, source.len)]; written += uuencode.encodeLineScalar(dest[written..], chunk, options).len; @memcpy(dest[written..][0..eol.len], eol); written += eol.len; } dest[written] = options.charset.zeroChar(); written += 1; @memcpy(dest[written..][0..eol.len], eol); written += eol.len; return dest[0..written];}
fn decodeScalar(dest: []u8, text: []const u8, options: zuucode.DecodeOptions) zuucode.DecodeError![]u8 { var written: usize = 0; var index: usize = 0; while (index < text.len) { const line_end = std.mem.indexOfScalarPos(u8, text, index, '\n') orelse text.len; const line = std.mem.trimEnd(u8, text[index..line_end], "\r"); index = line_end + 1;
if (line.len == 0 or uudecode.isEndLine(line)) break; if (uudecode.parseBeginLine(line) != null) continue;
const count = try uudecode.decodeLineScalar(dest[written..], line, options); if (count == 0) break; written += count; } return dest[0..written];}
// ------------------------------------------------- the comptime scalar control
/// The scalar line encoder with the charset fixed at comptime.fn encodeLineSpecialised( dest: []u8, source: []const u8, comptime charset: uuencode.Charset,) []u8 { const len = uuencode.lineLen(source.len);
dest[0] = encodeSix(@intCast(source.len), charset); var read: usize = 0; var written: usize = 1; while (read < source.len) : (read += 3) { const b0: u32 = source[read]; const b1: u32 = if (read + 1 < source.len) source[read + 1] else 0; const b2: u32 = if (read + 2 < source.len) source[read + 2] else 0; const group = (b0 << 16) | (b1 << 8) | b2;
dest[written + 0] = encodeSix(@truncate(group >> 18), charset); dest[written + 1] = encodeSix(@truncate(group >> 12), charset); dest[written + 2] = encodeSix(@truncate(group >> 6), charset); dest[written + 3] = encodeSix(@truncate(group), charset); written += 4; } return dest[0..len];}
fn encodeSix(six: u6, comptime charset: uuencode.Charset) u8 { return switch (charset) { .space => @as(u8, six) + ' ', .backtick => if (six == 0) '`' else @as(u8, six) + ' ', };}
fn encodeSpecialised(dest: []u8, source: []const u8, options: uuencode.Options) []u8 { switch (options.charset) { inline else => |charset| switch (options.line_ending) { inline else => |line_ending| { const eol = comptime line_ending.bytes(); var read: usize = 0; var written: usize = 0; while (read < source.len) : (read += bytes_per_line) { const chunk = source[read..@min(read + bytes_per_line, source.len)]; written += encodeLineSpecialised(dest[written..], chunk, charset).len; @memcpy(dest[written..][0..eol.len], eol); written += eol.len; } dest[written] = charset.zeroChar(); written += 1; @memcpy(dest[written..][0..eol.len], eol); written += eol.len; return dest[0..written]; }, }, }}
// ------------------------------------------------------------------ harness
const EncodeFn = *const fn ([]u8, []const u8, uuencode.Options) []u8;const DecodeFn = *const fn ([]u8, []const u8, zuucode.DecodeOptions) zuucode.DecodeError![]u8;
fn timeEncode( io: Io, encode: EncodeFn, dest: []u8, source: []const u8, options: uuencode.Options, reps: usize,) u64 { var best: u64 = std.math.maxInt(u64); for (0..trials) |_| { const start: Io.Timestamp = .now(io, .awake); var sink: u8 = 0; for (0..reps) |_| { const out = encode(dest, source, options); sink ^= out[out.len / 2]; std.mem.doNotOptimizeAway(out.ptr); } const ns: u64 = @intCast(start.durationTo(.now(io, .awake)).nanoseconds); std.mem.doNotOptimizeAway(sink); best = @min(best, ns); } return best;}
fn timeDecode( io: Io, decode: DecodeFn, dest: []u8, text: []const u8, options: zuucode.DecodeOptions, reps: usize,) !u64 { var best: u64 = std.math.maxInt(u64); for (0..trials) |_| { const start: Io.Timestamp = .now(io, .awake); var sink: u8 = 0; for (0..reps) |_| { const out = try decode(dest, text, options); if (out.len != 0) sink ^= out[out.len / 2]; std.mem.doNotOptimizeAway(out.ptr); } const ns: u64 = @intCast(start.durationTo(.now(io, .awake)).nanoseconds); std.mem.doNotOptimizeAway(sink); best = @min(best, ns); } return best;}
fn throughput(payload: usize, reps: usize, ns: u64) f64 { const bytes: f64 = @floatFromInt(payload * reps); const seconds = @as(f64, @floatFromInt(ns)) / 1e9; return bytes / seconds / (1 << 20);}
fn sizeLabel(buf: []u8, n: usize) []const u8 { if (n >= 1 << 20) return std.fmt.bufPrint(buf, "{d} MiB", .{n >> 20}) catch unreachable; if (n >= 1 << 10) return std.fmt.bufPrint(buf, "{d} KiB", .{n >> 10}) catch unreachable; return std.fmt.bufPrint(buf, "{d} B", .{n}) catch unreachable;}
/// Kernel benchmarks rotate through this many distinct lines. Enough that the/// compiler cannot treat the call as loop invariant and hoist it away, few/// enough that the whole table stays in L1.const table_lines = 64;
fn lineAt(source: []const u8, i: usize) *const [bytes_per_line]u8 { return source[(i % table_lines) * bytes_per_line ..][0..bytes_per_line];}
fn encodedLineAt(lines: *const [table_lines][zuucode.chars_per_line]u8, i: usize) []const u8 { return &lines[i % table_lines];}
const sizes = [_]usize{ 45, 1 << 10, 1 << 16, 1 << 20, 16 << 20 };
pub fn main(init: std.process.Init) !void { const io = init.io; const gpa = init.gpa;
var stdout_buffer: [4096]u8 = undefined; var stdout_writer: Io.File.Writer = .init(.stdout(), io, &stdout_buffer); const out = &stdout_writer.interface;
try out.print("vector kernels: {s} ({t})\n", .{ if (zuucode.simd_available) "enabled" else "disabled, scalar only", @import("builtin").cpu.arch, });
var prng: std.Random.DefaultPrng = .init(0xBE0C_4A55); const random = prng.random();
const largest = sizes[sizes.len - 1]; const source = try gpa.alloc(u8, largest); defer gpa.free(source); random.bytes(source);
for ([_]uuencode.Charset{ .backtick, .space }) |charset| { const options: uuencode.Options = .{ .charset = charset, .line_ending = .lf };
const encoded_cap = uuencode.encodedLen(largest, options); const encoded = try gpa.alloc(u8, encoded_cap); defer gpa.free(encoded); const decoded = try gpa.alloc(u8, largest); defer gpa.free(decoded);
try out.print("\nencode, {t} charset, LF line endings\n", .{charset}); try out.print(" {s:>8} {s:>12} {s:>12} {s:>12} {s:>8}\n", .{ "size", "scalar", "scalar+ct", "library", "speedup", });
for (sizes) |size| { const reps = @max(@as(usize, 3), target_bytes / size); const payload = source[0..size];
const a = timeEncode(io, encodeScalar, encoded, payload, options, reps); const b = timeEncode(io, encodeSpecialised, encoded, payload, options, reps); const c = timeEncode(io, zuucode.encode, encoded, payload, options, reps);
var label: [16]u8 = undefined; try out.print(" {s:>8} {d:>8.0} MB/s {d:>8.0} MB/s {d:>8.0} MB/s {d:>7.2}x\n", .{ sizeLabel(&label, size), throughput(size, reps, a), throughput(size, reps, b), throughput(size, reps, c), @as(f64, @floatFromInt(a)) / @as(f64, @floatFromInt(c)), }); }
try out.print("\ndecode, {t} charset, LF line endings\n", .{charset}); try out.print(" {s:>8} {s:>12} {s:>12} {s:>8}\n", .{ "size", "scalar", "library", "speedup", });
for (sizes) |size| { const reps = @max(@as(usize, 3), target_bytes / size); const text = uuencode.encode(encoded, source[0..size], options);
const a = try timeDecode(io, decodeScalar, decoded, text, .{}, reps); const c = try timeDecode(io, zuucode.decode, decoded, text, .{}, reps);
var label: [16]u8 = undefined; try out.print(" {s:>8} {d:>8.0} MB/s {d:>8.0} MB/s {d:>7.2}x\n", .{ sizeLabel(&label, size), throughput(size, reps, a), throughput(size, reps, c), @as(f64, @floatFromInt(a)) / @as(f64, @floatFromInt(c)), }); } }
// One line at a time, everything L1 resident. This is the ceiling the // kernels can reach: no line iteration, no bookkeeping, just the sextet // arithmetic. Comparing it with the whole-buffer figures above shows how // much of the win the surrounding loop gives back. try out.print("\nkernels, one 45 byte line, L1 resident\n", .{}); try out.print(" {s:>18} {s:>12} {s:>12} {s:>8}\n", .{ "operation", "scalar", "simd", "speedup", });
{ const kernel_reps = target_bytes / bytes_per_line;
var scalar_out: [zuucode.chars_per_line]u8 = undefined; var wide_out: [simd.chars_per_full_line]u8 = undefined;
var scalar_ns: u64 = std.math.maxInt(u64); var simd_ns: u64 = std.math.maxInt(u64); for (0..trials) |_| { var start: Io.Timestamp = .now(io, .awake); for (0..kernel_reps) |i| { _ = uuencode.encodeLineScalar(&scalar_out, lineAt(source, i), .{}); std.mem.doNotOptimizeAway(&scalar_out); } scalar_ns = @min(scalar_ns, @as(u64, @intCast(start.durationTo(.now(io, .awake)).nanoseconds)));
start = .now(io, .awake); for (0..kernel_reps) |i| { simd.encodeFullLine(&wide_out, lineAt(source, i), .backtick); std.mem.doNotOptimizeAway(&wide_out); } simd_ns = @min(simd_ns, @as(u64, @intCast(start.durationTo(.now(io, .awake)).nanoseconds))); } try out.print(" {s:>18} {d:>8.0} MB/s {d:>8.0} MB/s {d:>7.2}x\n", .{ "encode line", throughput(bytes_per_line, kernel_reps, scalar_ns), throughput(bytes_per_line, kernel_reps, simd_ns), @as(f64, @floatFromInt(scalar_ns)) / @as(f64, @floatFromInt(simd_ns)), });
var lines: [table_lines][zuucode.chars_per_line]u8 = undefined; for (&lines, 0..) |*encoded_line, i| { _ = uuencode.encodeLineScalar(encoded_line, lineAt(source, i), .{}); } var back: [bytes_per_line]u8 = undefined;
scalar_ns = std.math.maxInt(u64); simd_ns = std.math.maxInt(u64); for (0..trials) |_| { var start: Io.Timestamp = .now(io, .awake); for (0..kernel_reps) |i| { _ = try zuucode.decodeLineScalar(&back, encodedLineAt(&lines, i), .{}); std.mem.doNotOptimizeAway(&back); } scalar_ns = @min(scalar_ns, @as(u64, @intCast(start.durationTo(.now(io, .awake)).nanoseconds)));
start = .now(io, .awake); for (0..kernel_reps) |i| { _ = simd.decodeFullLine(&back, encodedLineAt(&lines, i)[1..][0..simd.chars_per_full_line]); std.mem.doNotOptimizeAway(&back); } simd_ns = @min(simd_ns, @as(u64, @intCast(start.durationTo(.now(io, .awake)).nanoseconds))); } try out.print(" {s:>18} {d:>8.0} MB/s {d:>8.0} MB/s {d:>7.2}x\n", .{ "decode line", throughput(bytes_per_line, kernel_reps, scalar_ns), throughput(bytes_per_line, kernel_reps, simd_ns), @as(f64, @floatFromInt(scalar_ns)) / @as(f64, @floatFromInt(simd_ns)), }); }
try out.flush();}