Performance of reading a file line by line in Zig #71
jiacai2050
started this conversation in
作品分享
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
-
Maybe I'm wrong, but I believe the canonical way to read a file, line by line, in Zig is:
We could use one of
reader
's thin wrappers aroundstreamUntilDelimiter
to make the code a little neater, but since our focus is on performance, we'll stick with less abstraction.The equivalent (I hope) Go code is:
What's interesting to me is that, on my computer, the Go version runs more than 4x faster. Using ReleaseFast with this 24MB json-line I found, Zig takes roughly 95ms whereas Go only takes 20ms. What gives?
The issue comes down to how the
std.io.Reader
functions, likestreamUntilDelimiter
are implemented and how that integrates withBufferedReader
. Much like Go'sio.Reader
, Zig'sstd.io.Reader
requires implementations to provide a single function:fn read(buffer: []u8) !usize
. Any functionality provided bystd.io.Reader
has to rely on this singleread
function.This is a fair representation of
std.io.Reader.streamUntilDelimiter
along with thereadByte
it depends on:This implementation will safely work for any type that implements a functional
read(buffer: []u8) !usize)
. But by targeting the lowest common denominator, we potentially lose a lot of performance. If you knew that the underlying implementation had a buffer you could come up with a much more efficient solutions. The biggest, but not only, performance gain would be to leverage the SIMD-optimizedstd.mem.indexOfScalar
to scan fordelimiter
over the entire buffer.Here's what that might look like:
If you're curious why
buffer
is ananytype
, it's becausestd.io.BufferedReader
is a generic, and we want ourstreamUntilDelimiter
for any variant, regardless of the type of the underlying unbuffered reader.If you take this function and use it in a similar way as our initial code, circumventing
std.io.Reader.streamUntilDelimiter
, you end up with similar performance as Go. And we'd still have room for some optimizations.This is something I tried to fix in Zig's standard library. I thought I could use Zig's comptime capabilities to detect if the underlying implementation has its own
streamUntilDelimeter
and use it, falling back tostd.io.Reader
's implementation otherwise. And while this is certainly possible, usingstd.meta.trait.hasFn
for example, I ran into problems that I just couldn't work around. The issue is that thebuffered.reader()
doesn't return anstd.io.Reader
directly, but goes through an intermediary:std.io.GenericReader
. ThisGenericReader
then creates anstd.io.Reader
on each function call. This double layer of abstraction was more than I wanted to, and probably could, work through.Instead I opened an issue and wrote a more generic zig utility library for the little Zig snippets I've collected.
I'm not sure how big the issue actually is. If we assume the above code is right and using a
BufferedReader
via anstd.io.Reader
is inefficient, then it's at least a real issue for this common task (on my initial real-world input which is where I ran into this issue, the overhead was over 10x). But the "interface" pattern of building functionality atop the lowest common denominator is common, so I wonder where else performance is being lost. In this specific case though, I think there's an argument to be made that functionality likestreamUntilDelimeter
should only be made available on something like aBufferedReader
.Beta Was this translation helpful? Give feedback.
All reactions