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
  
        
    
Uh oh!
There was an error while loading. Please reload this page.
-
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 aroundstreamUntilDelimiterto 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.Readerfunctions, likestreamUntilDelimiterare implemented and how that integrates withBufferedReader. Much like Go'sio.Reader, Zig'sstd.io.Readerrequires implementations to provide a single function:fn read(buffer: []u8) !usize. Any functionality provided bystd.io.Readerhas to rely on this singlereadfunction.This is a fair representation of
std.io.Reader.streamUntilDelimiteralong with thereadByteit 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.indexOfScalarto scan fordelimiterover the entire buffer.Here's what that might look like:
If you're curious why
bufferis ananytype, it's becausestd.io.BufferedReaderis a generic, and we want ourstreamUntilDelimiterfor 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
streamUntilDelimeterand use it, falling back tostd.io.Reader's implementation otherwise. And while this is certainly possible, usingstd.meta.trait.hasFnfor example, I ran into problems that I just couldn't work around. The issue is that thebuffered.reader()doesn't return anstd.io.Readerdirectly, but goes through an intermediary:std.io.GenericReader. ThisGenericReaderthen creates anstd.io.Readeron 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
BufferedReadervia anstd.io.Readeris 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 likestreamUntilDelimetershould only be made available on something like aBufferedReader.Beta Was this translation helpful? Give feedback.
All reactions