Skip to content

Conversation

@andrewrk
Copy link
Member

@andrewrk andrewrk commented Jul 4, 2025

Previous Scandal

Summary

Deprecates all existing std.io readers and writers in favor of the newly provided std.Io.Reader and std.Io.Writer which are non-generic and have the buffer above the vtable - in other words the buffer is in the interface, not the implementation. This means that although Reader and Writer are no longer generic, they are still transparent to optimization; all of the interface functions have a concrete hot path operating on the buffer, and only make vtable calls when the buffer is full.

I have a lot more changes to upstream but it was taking too long to finish them so I decided to do it more piecemeal. Therefore, I opened this tiny baby PR to get things started.

These changes are extremely breaking. I am sorry for that, but I have carefully examined the situation and acquired confidence that this is the direction that Zig needs to go. I hope you will strap in your seatbelt and come along for the ride; it will be worth it.

The breakage in this first PR mainly has to do with formatted printing.

More Detailed Motivation

I wrote this up for ziggit but I think it would be good to include in release notes:

  • The old interface was generic, poisoning structs that contain them and forcing all functions to be generic as well with anytype. The new interface is concrete.
    • Bonus: the concreteness removes temptation to make APIs operate directly on networking streams, file handles, or memory buffers, giving us a more reusable body of code. For example, http.Server after the change no longer depends on std.net - it operates only on streams now.
  • The old interface passed errors through rather than defining its own set of error codes. This made errors in streams about as useful as anyerror. The new interface carefully defines precise error sets for each function with actionable meaning.
  • The new interface has the buffer in the interface, rather than as a separate "BufferedReader" / "BufferedWriter" abstraction. This is more optimizer friendly, particularly for debug mode.
  • The new interface supports high level concepts such as vectors, splatting, and direct file-to-file transfer, which can propagate through an entire graph of readers and writers, reducing syscall overhead, memory bandwidth, and CPU usage.
  • The new interface has "peek" functionality - a buffer awareness that offers API convenience for the user as well as simplicity for the implementation.

Performance Data

Building Self-Hosted Compiler with Itself

Benchmark 1 (3 runs): master/fast/bin/zig build-exe ...
  measurement          mean ± σ            min … max           outliers         delta
  wall_time          12.4s  ± 49.3ms    12.3s  … 12.4s           0 ( 0%)        0%
  peak_rss           1.03GB ± 4.67MB    1.02GB … 1.03GB          0 ( 0%)        0%
  cpu_cycles          105G  ±  323M      105G  …  105G           0 ( 0%)        0%
  instructions        207G  ± 4.41M      207G  …  207G           0 ( 0%)        0%
  cache_references   6.62G  ± 23.9M     6.60G  … 6.64G           0 ( 0%)        0%
  cache_misses        449M  ± 3.17M      447M  …  453M           0 ( 0%)        0%
  branch_misses       411M  ± 1.62M      409M  …  412M           0 ( 0%)        0%
Benchmark 2 (3 runs): writergate/fast/bin/zig build-exe ...
  measurement          mean ± σ            min … max           outliers         delta
  wall_time          10.6s  ± 28.6ms    10.5s  … 10.6s           0 ( 0%)        ⚡- 14.6% ±  0.7%
  peak_rss           1.14GB ± 5.26MB    1.14GB … 1.15GB          0 ( 0%)        💩+ 10.8% ±  1.1%
  cpu_cycles         95.0G  ± 19.8M     95.0G  … 95.1G           0 ( 0%)        ⚡-  9.6% ±  0.5%
  instructions        191G  ± 2.22M      191G  …  191G           0 ( 0%)        ⚡-  7.8% ±  0.0%
  cache_references   5.68G  ± 13.9M     5.66G  … 5.69G           0 ( 0%)        ⚡- 14.2% ±  0.7%
  cache_misses        386M  ± 2.47M      384M  …  388M           0 ( 0%)        ⚡- 14.2% ±  1.4%
  branch_misses       400M  ±  516K      400M  …  401M           0 ( 0%)        ⚡-  2.6% ±  0.7%

Building My Music Player Project

source

Benchmark 1 (3 runs): master/stage3/bin/zig build-exe ...
  measurement          mean ± σ            min … max           outliers         delta
  wall_time          1.86s  ± 36.3ms    1.84s  … 1.90s           0 ( 0%)        0%
  peak_rss            798MB ± 3.84MB     796MB …  803MB          0 ( 0%)        0%
  cpu_cycles         11.0G  ± 24.0M     11.0G  … 11.1G           0 ( 0%)        0%
  instructions       28.5G  ±  796K     28.5G  … 28.5G           0 ( 0%)        0%
  cache_references    610M  ± 1.41M      609M  …  611M           0 ( 0%)        0%
  cache_misses       52.8M  ±  559K     52.2M  … 53.2M           0 ( 0%)        0%
  branch_misses      49.7M  ±  366K     49.3M  … 50.1M           0 ( 0%)        0%
Benchmark 2 (3 runs): writergate/bin/zig build-exe ...
  measurement          mean ± σ            min … max           outliers         delta
  wall_time          2.09s  ± 2.02ms    2.09s  … 2.09s           0 ( 0%)        💩+ 12.4% ±  3.1%
  peak_rss            800MB ±  693KB     800MB …  801MB          0 ( 0%)          +  0.3% ±  0.8%
  cpu_cycles         12.4G  ± 41.7M     12.4G  … 12.4G           0 ( 0%)        💩+ 12.5% ±  0.7%
  instructions       30.4G  ±  472K     30.4G  … 30.4G           0 ( 0%)        💩+  6.8% ±  0.0%
  cache_references    615M  ± 2.47M      612M  …  617M           0 ( 0%)          +  0.9% ±  0.7%
  cache_misses       53.9M  ± 1.11M     52.9M  … 55.1M           0 ( 0%)          +  2.1% ±  3.8%
  branch_misses      46.5M  ±  179K     46.4M  … 46.7M           0 ( 0%)        ⚡-  6.3% ±  1.3%

Compiler Binary Size (ReleaseSmall)

  • x86_64: 13.6 -> 13.3 MiB (-2%)
  • zig1.wasm: 2.8 -> 2.7 MiB (-4%)

C Backend Building the Zig Compiler

Benchmark 1 (3 runs): master/bin/zig build-exe -ofmt=c ...writergate source tree...
  measurement          mean ± σ            min … max           outliers         delta
  wall_time          2.67s  ± 40.5ms    2.65s  … 2.72s           0 ( 0%)        0%
  peak_rss            572MB ± 1.13MB     571MB …  573MB          0 ( 0%)        0%
  cpu_cycles         37.3G  ±  190M     37.2G  … 37.5G           0 ( 0%)        0%
  instructions       72.8G  ± 3.21M     72.8G  … 72.8G           0 ( 0%)        0%
  cache_references   1.90G  ± 9.27M     1.89G  … 1.91G           0 ( 0%)        0%
  cache_misses        131M  ± 1.29M      130M  …  132M           0 ( 0%)        0%
  branch_misses       146M  ±  161K      146M  …  146M           0 ( 0%)        0%
Benchmark 2 (3 runs): writergate/bin/zig build-exe -ofmt=c ...writergate source tree...
  measurement          mean ± σ            min … max           outliers         delta
  wall_time          2.70s  ± 19.5ms    2.68s  … 2.72s           0 ( 0%)          +  0.8% ±  2.7%
  peak_rss            572MB ±  208KB     572MB …  572MB          0 ( 0%)          -  0.0% ±  0.3%
  cpu_cycles         36.4G  ±  253M     36.2G  … 36.6G           0 ( 0%)          -  2.3% ±  1.4%
  instructions       69.8G  ± 4.18M     69.8G  … 69.8G           0 ( 0%)        ⚡-  4.1% ±  0.0%
  cache_references   2.08G  ± 38.2M     2.04G  … 2.12G           0 ( 0%)        💩+  9.5% ±  3.3%
  cache_misses        134M  ± 2.81M      131M  …  136M           0 ( 0%)          +  2.1% ±  3.8%
  branch_misses       143M  ±  486K      142M  …  143M           0 ( 0%)        ⚡-  2.5% ±  0.6%

C Backend Building Hello World

ReleaseFast zig

Benchmark 1 (27 runs): master/stage3/bin/zig build-exe master/zig/test/standalone/simple/hello_world/hello.zig -ofmt=c
  measurement          mean ± σ            min … max           outliers         delta
  wall_time           187ms ± 4.08ms     180ms …  194ms          0 ( 0%)        0%
  peak_rss            129MB ±  553KB     129MB …  131MB          0 ( 0%)        0%
  cpu_cycles         1.49G  ± 11.3M     1.47G  … 1.53G           2 ( 7%)        0%
  instructions       2.70G  ±  252K     2.70G  … 2.70G           0 ( 0%)        0%
  cache_references    101M  ±  498K      101M  …  103M           1 ( 4%)        0%
  cache_misses       8.70M  ±  194K     8.14M  … 9.18M           2 ( 7%)        0%
  branch_misses      8.32M  ± 91.4K     8.07M  … 8.46M           1 ( 4%)        0%
Benchmark 2 (35 runs): writergate/bin/zig build-exe writergate/zig/test/standalone/simple/hello_world/hello.zig -ofmt=c
  measurement          mean ± σ            min … max           outliers         delta
  wall_time           145ms ± 5.17ms     137ms …  160ms          0 ( 0%)        ⚡- 22.4% ±  1.3%
  peak_rss            128MB ±  518KB     127MB …  129MB          0 ( 0%)          -  0.8% ±  0.2%
  cpu_cycles         1.17G  ± 11.4M     1.16G  … 1.20G           0 ( 0%)        ⚡- 21.5% ±  0.4%
  instructions       2.07G  ±  210K     2.07G  … 2.07G           0 ( 0%)        ⚡- 23.3% ±  0.0%
  cache_references   81.4M  ±  478K     80.5M  … 82.6M           0 ( 0%)        ⚡- 19.7% ±  0.2%
  cache_misses       7.21M  ±  152K     6.91M  … 7.47M           0 ( 0%)        ⚡- 17.1% ±  1.0%
  branch_misses      7.40M  ± 64.0K     7.27M  … 7.54M           0 ( 0%)        ⚡- 11.1% ±  0.5%

Debug zig

Benchmark 1 (3 runs): master/bin/zig build-exe master/zig/test/standalone/simple/hello_world/hello.zig -ofmt=c
  measurement          mean ± σ            min … max           outliers         delta
  wall_time          17.2s  ± 16.7ms    17.1s  … 17.2s           0 ( 0%)        0%
  peak_rss            245MB ± 1.04MB     244MB …  246MB          0 ( 0%)        0%
  cpu_cycles         54.4G  ±  150M     54.2G  … 54.5G           0 ( 0%)        0%
  instructions       40.8G  ± 3.62M     40.8G  … 40.8G           0 ( 0%)        0%
  cache_references   2.65G  ± 5.59M     2.65G  … 2.66G           0 ( 0%)        0%
  cache_misses        408M  ± 1.97M      406M  …  410M           0 ( 0%)        0%
  branch_misses       190M  ±  103K      190M  …  190M           0 ( 0%)        0%
Benchmark 2 (3 runs): writergate/bin/zig build-exe writergate/zig/test/standalone/simple/hello_world/hello.zig -ofmt=c
  measurement          mean ± σ            min … max           outliers         delta
  wall_time          16.1s  ± 43.4ms    16.0s  … 16.1s           0 ( 0%)        ⚡-  6.3% ±  0.4%
  peak_rss            244MB ±  695KB     244MB …  245MB          0 ( 0%)          -  0.1% ±  0.8%
  cpu_cycles         44.3G  ±  139M     44.2G  … 44.4G           0 ( 0%)        ⚡- 18.5% ±  0.6%
  instructions       33.4G  ±  437K     33.4G  … 33.4G           0 ( 0%)        ⚡- 18.2% ±  0.0%
  cache_references   2.19G  ± 11.5M     2.18G  … 2.20G           0 ( 0%)        ⚡- 17.2% ±  0.8%
  cache_misses        339M  ± 6.31M      332M  …  345M           0 ( 0%)        ⚡- 16.9% ±  2.6%
  branch_misses       185M  ±  395K      184M  …  185M           0 ( 0%)        ⚡-  2.7% ±  0.3%

Upgrade Guide

Turn on -freference-trace to help you find all the format string breakage.

"{f}" Required to Call format Methods

Example:

std.debug.print("{}", .{std.zig.fmtId("example")});

This will now cause a compile error:

error: ambiguous format string; specify {f} to call format method, or {any} to skip it

Fixed by:

std.debug.print("{f}", .{std.zig.fmtId("example")});

Motivation: eliminate these two footguns:

Introducing a format method to a struct caused a bug if there was formatting code somewhere that prints with {} and then starts rendering differently.

Removing a format method to a struct caused a bug if there was formatting code somewhere that prints with {} and is now changed without notice.

Now, introducing a format method will cause compile errors at all {} sites. In the future, it will have no effect.

Similarly, eliminating a format method will not change any sites that use {}.

Using {f} always tries to call a format method, causing a compile error if none exists.

Format Methods No Longer Have Format Strings or Options

pub fn format(
    this: @This(),
    comptime format_string: []const u8,
    options: std.fmt.FormatOptions,
    writer: anytype,
) !void { ... }

⬇️

pub fn format(this: @This(), writer: *std.io.Writer) std.io.Writer.Error!void { ... }

The deleted FormatOptions are now for numbers only.

Any state that you got from the format string, there are three suggested alternatives:

  1. different format methods
pub fn formatB(foo: Foo, writer: *std.io.Writer) std.io.Writer.Error!void { ... }

This can be called with "{f}", .{std.fmt.alt(Foo, .formatB)}.

  1. std.fmt.Alt
pub fn bar(foo: Foo, context: i32) std.fmt.Alt(F, F.baz) {
    return .{ .data = .{ .context = context } };
}
const F = struct {
    context: i32,
    pub fn baz(f: F, writer: *std.io.Writer) std.io.Writer.Error!void { ... }
};

This can be called with "{f}", .{foo.bar(1234)}.

  1. return a struct instance that has a format method, combined with {f}.
pub fn bar(foo: Foo, context: i32) F {
    return .{ .context = 1234 };
}
const F = struct {
    context: i32,
    pub fn format(f: F, writer: *std.io.Writer) std.io.Writer.Error!void { ... }
};

This can be called with "{f}", .{foo.bar(1234)}.

Formatted Printing No Longer Deals with Unicode

If you were relying on alignment combined with Unicode codepoints, it is now ASCII/bytes only. The previous implementation was not fully Unicode-aware. If you want to align Unicode strings you need full Unicode support which the standard library does not provide.

std.io.getStdOut().writer().print()

Please use buffering! And don't forget to flush!

var stdout_buffer: [1024]u8 = undefined;
var stdout_writer = std.fs.File.stdout().writer(&buffer);
const stdout = &stdout_writer.interface;

// ...

try stdout.print("...", .{});

// ...

try stdout.flush();

Miscellaneous

  • std.fs.File.reader -> std.fs.File.deprecatedReader
  • std.fs.File.writer -> std.fs.File.deprecatedWriter
  • std.fmt.fmtSliceEscapeLower -> std.ascii.hexEscape
  • std.fmt.fmtSliceEscapeUpper -> std.ascii.hexEscape
  • std.zig.fmtEscapes -> std.zig.fmtString
  • std.fmt.fmtSliceHexLower -> {x}
  • std.fmt.fmtSliceHexUpper -> {X}
  • std.fmt.fmtIntSizeDec -> {B}
  • std.fmt.fmtIntSizeBin -> {Bi}
  • std.fmt.fmtDuration -> {D}
  • std.fmt.fmtDurationSigned -> {D}
  • std.fmt.Formatter -> std.fmt.Alt
    • now takes context type explicitly
    • no fmt string

These are deprecated but not deleted yet:

  • std.fmt.format -> std.io.Writer.print
  • std.io.GenericReader -> std.io.Reader
  • std.io.GenericWriter -> std.io.Writer
  • std.io.AnyReader -> std.io.Reader
  • std.io.AnyWriter -> std.io.Writer

If you have an old stream and you need a new one, you can use adaptToNewApi() like this:

fn foo(old_writer: anytype) !void {
    var adapter = old_writer.adaptToNewApi();
    const w: *std.io.Writer = &adapter.new_interface;
    try w.print("{s}", .{"example"});
    // ...
}

New API

Formatted Printing

  • {t} is shorthand for @tagName() and @errorName()
  • {d} and other integer printing can be used with custom types which calls formatNumber method.
  • {b64}: output string as standard base64

std.io.Writer and std.io.Reader

These have a bunch of handy new APIs that are more convenient, perform better, and are not generic. For instance look at how reading until a delimiter works now.

These streams also feature some unique concepts compared with other languages' stream implementations:

  • The concept of discarding when reading: allows efficiently ignoring data. For instance a decompression stream, when asked to discard a large amount of data, can skip decompression of entire frames.
  • The concept of splatting when writing: this allows a logical "memset" operation to pass through I/O pipelines without actually doing any memory copying, turning an O(M*N) operation into O(M) operation, where M is the number of streams in the pipeline and N is the number of repeated bytes. In some cases it can be even more efficient, such as when splatting a zero value that ends up being written to a file; this can be lowered as a seek forward.
  • Sending a file when writing: this allows an I/O pipeline to do direct fd-to-fd copying when the operating system supports it.
  • The stream user provides the buffer, but the stream implementation decides the minimum buffer size. This effectively moves state from the stream implementation into the user's buffer

std.fs.File.Reader

Memoizes key information about a file handle such as:

  • The size from calling stat, or the error that occurred therein.
  • The current seek position.
  • The error that occurred when trying to seek.
  • Whether reading should be done positionally or streaming.
  • Whether reading should be done via fd-to-fd syscalls (e.g. sendfile)
    versus plain variants (e.g. read).

Fulfills the std.io.Reader interface.

This API turned out to be super handy in practice. Having a concrete type to pass around that memoizes file size is really nice.

std.fs.File.Writer

Same idea but for writing.

What's NOT Included in this Branch

This is part of a series of changes leading up to "I/O as an Interface" and Async/Await Resurrection. However, this branch does not do any of that. It also does not do any of these things:

  • Rework tls
  • Rework http
  • Rework json
  • Rework zon
  • Rework zstd
  • Rework flate
  • Rework zip
  • Rework package fetching
  • Delete fifo.LinearFifo
  • Delete the deprecated APIs mentioned above

I have done all the above in a separate branch and plan to upstream them one at a time in follow-up PRs, eliminating dependencies on the old streaming APIs like a game of pick-up-sticks.

Merge Checklist:

  • bootstrapped compiler is crashing
  • Windows TODOs
  • fix bootstrapped stage3 compiler crashing
  • fix failing behavior tests
  • fix failing std lib tests
  • solve the TODOs in std.io.Writer
  • finish implementing std.fs.File.Writer which doesn't handle positional mode. this is probably breaking caching which now relies on the manifest being written positionally to save from having to seek.
  • eliminate Writer.count
  • update-zig1 is non viable
  • error for using alignment options when they're not observed
  • something about packed structs in Reader
  • formatNumber rather than formatInteger

@andrewrk andrewrk added breaking Implementing this issue could cause existing code to no longer compile or have different behavior. standard library This issue involves writing Zig code for the standard library. release notes This PR should be mentioned in the release notes. labels Jul 4, 2025
@andrewrk andrewrk force-pushed the writergate branch 3 times, most recently from 5837754 to 1ef243b Compare July 8, 2025 02:56
andrewrk and others added 15 commits July 7, 2025 22:43
Macos uses the BSD definition of msghdr

All linux architectures share a single msghdr definition. Many
architectures had manually inserted padding fields that were endian
specific and some had fields with different integers. This unifies all
architectures to use a single correct msghdr definition.
preparing to rearrange std.io namespace into an interface

how to upgrade:

std.io.getStdIn() -> std.fs.File.stdin()
std.io.getStdOut() -> std.fs.File.stdout()
std.io.getStdErr() -> std.fs.File.stderr()
added adapter to AnyWriter and GenericWriter to help bridge the gap
between old and new API

make std.testing.expectFmt work at compile-time

std.fmt no longer has a dependency on std.unicode. Formatted printing
was never properly unicode-aware. Now it no longer pretends to be.

Breakage/deprecations:
* std.fs.File.reader -> std.fs.File.deprecatedReader
* std.fs.File.writer -> std.fs.File.deprecatedWriter
* std.io.GenericReader -> std.io.Reader
* std.io.GenericWriter -> std.io.Writer
* std.io.AnyReader -> std.io.Reader
* std.io.AnyWriter -> std.io.Writer
* std.fmt.format -> std.fmt.deprecatedFormat
* std.fmt.fmtSliceEscapeLower -> std.ascii.hexEscape
* std.fmt.fmtSliceEscapeUpper -> std.ascii.hexEscape
* std.fmt.fmtSliceHexLower -> {x}
* std.fmt.fmtSliceHexUpper -> {X}
* std.fmt.fmtIntSizeDec -> {B}
* std.fmt.fmtIntSizeBin -> {Bi}
* std.fmt.fmtDuration -> {D}
* std.fmt.fmtDurationSigned -> {D}
* {} -> {f} when there is a format method
* format method signature
  - anytype -> *std.io.Writer
  - inferred error set -> error{WriteFailed}
  - options -> (deleted)
* std.fmt.Formatted
  - now takes context type explicitly
  - no fmt string
behavior tests must not depend on std.io
for structs, enums, and unions.

auto untagged unions are no longer printed as pointers; instead they are
printed as "{ ... }".

extern and packed untagged unions have each field printed, similar to
what gdb does.

also fix bugs in delimiter based reading
it didn't account for data.len can no longer be zero
now it avoids writing to buffer in the case of fixed
So that when returning from drain there is always capacity for at least
one more byte.
IntegratedQuantum added a commit to PixelGuys/Cubyz that referenced this pull request Jul 15, 2025
For reference: ziglang/zig#24329

some commits have been extracted from #1583, but the x86_64 backend has
been disabled due to its horrible performance.

Remaining work:
- [x] Wait for official builds on ziglang.org and upload them to our
repository
- [x] Add workaround for ziglang/zig#24466
- [x] Fix TODO comment about ANSI support in stdout
- [x] Check for compile-time performance changes → it went from 13.1 to
11.9 seconds 🎉
silbinarywolf added a commit to silbinarywolf/zig-android-sdk that referenced this pull request Jul 27, 2025
…ort) (#47)

**Why?**

Writer interface was changed and broken, need to update the Android SDK
to support 0.14.1 vs 0.15.x-dev

**Related to breaking change here:**
ziglang/zig#24329
dasimmet pushed a commit to dasimmet/zig that referenced this pull request Aug 4, 2025
don't forget to save the list.  this allows a
`testing.checkAllAllocationFailures()` test to pass in one of my
projects which newly failed since ziglang#24329 was merged.
@jcalabro jcalabro mentioned this pull request Aug 30, 2025
eliminmax added a commit to eliminmax/colortest that referenced this pull request Dec 24, 2025
Buffered printing to stdout in Zig now requires more explicit buffer
usage, so the Zig implementation now uses that, and the version of Zig
installed by `RUNNERS/install-deps.sh` can be updated to the latest
release.

see ziglang/zig#24329
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

breaking Implementing this issue could cause existing code to no longer compile or have different behavior. release notes This PR should be mentioned in the release notes. standard library This issue involves writing Zig code for the standard library.

Projects

None yet

Development

Successfully merging this pull request may close these issues.