-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Let Zlib::Reader, Gzip::Reader and Flate::Reader include IO::Buffered for optimized performance #6916
Conversation
Can be composition of IOs used to reach the performance? It would be great to keep IO as orthogonal as possible IMO. If we need to chain two or more IOs that happen to be all buffered then we will end with intermediate buffers that will not be used at all and just filling memory I think. |
@bcardiff Yes, for that we need to remove the On the other hand, if you have a |
Actually, you can try it by setting |
Actually, setting both file and gzip::reader to |
I just added another commit that fixes With that, if I set
Anyway, I think adding buffering at each level is good. And, again, 8KB pero IO is not a big deal, considering that the OS can't have many file descriptors open at once. |
By the way, with this commit I also fixed |
Yeah, it makes a difference to have multiple buffers in the IO chain. All potentially costly read operations should be buffered to speed things up.
I've taken a quick look at the other IO implementations.
HTTP content IOs look fine, though. They mostly pass through. A buffer is only useful if there is a significant performance overhead between 1000 |
@straight-shoota Thank you for the research! I think buffering is not needed for wrapper as long as they provide a good |
Relying on the user to |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you @asterite 👍
This is fine to implement now that we have |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And ditto for all the other impls
@@ -119,8 +125,12 @@ class Flate::Reader < IO | |||
end | |||
end | |||
|
|||
def unbuffered_flush | |||
raise IO::Error.new "Can't flush Flate::Reader" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't this be: io.flush
? It makes no sense to raise here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well, it's a reader, but yeah, we can delegate
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do other readers raise on flush? I thought the default semantic for flush was to silently ignore if it has nothing to do.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, other reads raise on flush.
@@ -130,6 +140,14 @@ class Flate::Reader < IO | |||
@io.close if @sync_close | |||
end | |||
|
|||
def unbuffered_rewind | |||
raise IO::Error.new "Closed stream" if closed? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
check_open
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks! I updated the code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not just use check_open
call like @RX14 suggested?
Reading from any I'm not sure |
@RX14 |
@RX14 @straight-shoota Yeah, probably the buffer isn't needed for gzip and zlib, but if I don't include |
with with |
But:
I think it's better for me to close this PR and you can work on all of these things because I won't have time to update this PR with more changes. |
@asterite notice that in
|
No, |
Ah, that makes sense. I'll try it later and update the code. |
Might as well keep this PR as-is then because of the 4KiB rule. Ultimately, this needs benchmarks to know what the right behaviour is. |
Yes, I think the current PR is fine. Feel free to merge it. |
@RX14 Ping. Other readers also raise on flush, or at least |
I'm not sure what you mean regarding flush, I'm talking about this failing: io.read_byte
# io now has a read buffer of size 8192, 8191 bytes are valid
io.read_buffering = false
io.gets
# io.gets will return only a maximum of 8191 bytes, even if the next `\n` or `\r\n` is beyond that size. |
For that I suggest removing |
Or we can fix |
… for optimized performance
06453a1
to
a84c71d
Compare
Okay, I removed the |
It's fine to remove that fix for now but the bug still persists and should be opened in another issue. |
return if @closed | ||
@closed = true | ||
|
||
@flate_io.try &.close | ||
@io.close if @sync_close | ||
end | ||
|
||
def unbuffered_rewind | ||
raise IO::Error.new "Closed stream" if closed? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ditto
return if @closed | ||
@closed = true | ||
|
||
@flate_io.close | ||
@io.close if @sync_close | ||
end | ||
|
||
def unbuffered_rewind | ||
raise IO::Error.new "Closed stream" if closed? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ditto
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, it was like that but I had to rollback to a previous version and I forgot to add that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Someone can send a PR, or commit directly to master with it (I'm trying but I can't).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
#6958 :)
All of these readers wrap another IO from which they read. The problem is that when using methods like
gets
which read until some character is found, usually a newline, it's much faster to use input buffering to read a lot of bytes at once instead of going byte per byte (or worse, utf-8 char per char). Thegets
method actually relies on apeek
method to peek next bytes if available to perform the scan faster, and the easiest way to accomplish this is by includingIO::Buffered
, which implements this.(There might be a way to implement this without include
IO::Buffered
, maybe for example inZlib::Reader
having an output buffer similar to the one inIO::Buffered
, and reading there and copying from that to the requested slice byread
, but that's basically duplicating the work that's done inIO::Buffered
, so it's probably not worth it).Note that this will require an additional 8KB of memory for each of these IOs, but that's fine because one usually doesn't deal with a lot of IOs at a time.
This optimization happened because the slowness was discovered in IRC.
Also checking what Python does, their gzip reader is buffered too (and I guess that's the case for Ruby too).
(An additional research, if someone wants to tackle this, is checking whether other IOs in the std, notably HTTP responses, might need this optimization too.)
Benchmark follows:
Old output:
New output:
Gzip: 29 times faster
Zlib: 25 times faster
Flate: 21 times faster