diff --git a/CHANGELOG.md b/CHANGELOG.md index affaffe..3daab89 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Encoding writes straight into the writer's buffer when there is room, and keeps the drain path out of line, which is about 13% cheaper on a typical struct and 37% on an array of integers +- Encoding a struct is about 25% cheaper again, by telling the compiler that writing into the buffer cannot change the writer itself ## [0.9.0] - 2026-09-05 diff --git a/src/string.zig b/src/string.zig index adb8b8b..a178686 100644 --- a/src/string.zig +++ b/src/string.zig @@ -67,7 +67,7 @@ pub fn packString(writer: *std.Io.Writer, value_or_maybe_null: ?[]const u8) !voi /// Packs a string whose contents are known at comptime, such as a struct field /// name used as a map key. The header and the bytes are one contiguous store of /// comptime-known length, so this compiles to immediate stores with no `memcpy`. -pub fn packStringLiteral(writer: *std.Io.Writer, comptime value: []const u8) !void { +pub fn packStringLiteral(noalias writer: *std.Io.Writer, comptime value: []const u8) !void { const bytes = comptime blk: { if (value.len <= hdrs.FIXSTR_MAX - hdrs.FIXSTR_MIN) { break :blk [_]u8{hdrs.FIXSTR_MIN + @as(u8, value.len)} ++ value[0..].*; diff --git a/src/utils.zig b/src/utils.zig index b7d5f1b..8dc7b01 100644 --- a/src/utils.zig +++ b/src/utils.zig @@ -24,7 +24,7 @@ test isOptional { /// Like `Writer.writeByte`, but with the drain moved out of line, which keeps /// the function small enough to inline into every caller. -pub fn writeByte(writer: *std.Io.Writer, byte: u8) !void { +pub fn writeByte(noalias writer: *std.Io.Writer, byte: u8) !void { if (writer.end < writer.buffer.len) { @branchHint(.likely); writer.buffer[writer.end] = byte; @@ -38,7 +38,14 @@ pub fn writeByte(writer: *std.Io.Writer, byte: u8) !void { /// `Writer.writeAll` otherwise. `Writer.writeAll` loops over `write`, so the /// copy goes through an out-of-line `memcpy` even for a few bytes; checking for /// room first lets the copy inline. -pub fn writeAll(writer: *std.Io.Writer, bytes: []const u8) !void { +/// +/// The writer is `noalias`, here and in the other helpers that store into its +/// buffer. Without it, the compiler has to assume that a byte stored into the +/// buffer may have changed the writer's own fields, and reloads `end` and the +/// buffer after every write, which turns a run of small writes into a chain of +/// dependent loads and stores. Nothing may change the writer through another +/// pointer during the call. +pub fn writeAll(noalias writer: *std.Io.Writer, bytes: []const u8) !void { if (writer.end + bytes.len <= writer.buffer.len) { @branchHint(.likely); @memcpy(writer.buffer[writer.end..][0..bytes.len], bytes); @@ -92,7 +99,7 @@ pub inline fn eqlLiteral(comptime name: []const u8, value: []const u8) bool { /// Writes a header byte followed by a big-endian `T`, as a single contiguous /// store when the writer's buffer has room, and as a single `writeAll` /// otherwise. Never splits the header from its payload across a drain. -pub fn packHeaderAndInt(writer: *std.Io.Writer, header: u8, comptime T: type, value: T) !void { +pub fn packHeaderAndInt(noalias writer: *std.Io.Writer, header: u8, comptime T: type, value: T) !void { const len = 1 + @sizeOf(T); if (writer.end + len <= writer.buffer.len) { @branchHint(.likely);