Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion src/string.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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..].*;
Expand Down
13 changes: 10 additions & 3 deletions src/utils.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);
Expand Down Expand Up @@ -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);
Expand Down
Loading