-
Notifications
You must be signed in to change notification settings - Fork 13.3k
Fix massive performance issue in read_to_end #23820
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -48,30 +48,6 @@ mod stdio; | |
|
||
const DEFAULT_BUF_SIZE: usize = 64 * 1024; | ||
|
||
// Acquires a slice of the vector `v` from its length to its capacity | ||
// (after initializing the data), reads into it, and then updates the length. | ||
// | ||
// This function is leveraged to efficiently read some bytes into a destination | ||
// vector without extra copying and taking advantage of the space that's already | ||
// in `v`. | ||
fn with_end_to_cap<F>(v: &mut Vec<u8>, f: F) -> Result<usize> | ||
where F: FnOnce(&mut [u8]) -> Result<usize> | ||
{ | ||
let len = v.len(); | ||
let new_area = v.capacity() - len; | ||
v.extend(iter::repeat(0).take(new_area)); | ||
match f(&mut v[len..]) { | ||
Ok(n) => { | ||
v.truncate(len + n); | ||
Ok(n) | ||
} | ||
Err(e) => { | ||
v.truncate(len); | ||
Err(e) | ||
} | ||
} | ||
} | ||
|
||
// A few methods below (read_to_string, read_line) will append data into a | ||
// `String` buffer, but we need to be pretty careful when doing this. The | ||
// implementation will just call `.as_mut_vec()` and then delegate to a | ||
|
@@ -116,19 +92,45 @@ fn append_to_string<F>(buf: &mut String, f: F) -> Result<usize> | |
} | ||
} | ||
|
||
// This uses an adaptive system to extend the vector when it fills. We want to | ||
// avoid paying to allocate and zero a huge chunk of memory if the reader only | ||
// has 4 bytes while still making large reads if the reader does have a ton | ||
// of data to return. Simply tacking on an extra DEFAULT_BUF_SIZE space every | ||
// time is 4,500 times (!) slower than this if the reader has a very small | ||
// amount of data to return. | ||
fn read_to_end<R: Read + ?Sized>(r: &mut R, buf: &mut Vec<u8>) -> Result<usize> { | ||
let mut read = 0; | ||
let start_len = buf.len(); | ||
let mut len = start_len; | ||
let mut cap_bump = 16; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Seems like this should be buf.capacity() rather than a constant. Otherwise, if the vector is large, you are going to be reallocating it a lot unnecessarily once the read reaches its capacity. |
||
let ret; | ||
loop { | ||
if buf.capacity() == buf.len() { | ||
buf.reserve(DEFAULT_BUF_SIZE); | ||
if len == buf.len() { | ||
if buf.capacity() == buf.len() { | ||
if cap_bump < DEFAULT_BUF_SIZE { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why have a maximum capacity increase at all? This makes the function O(number of bytes squared) instead of O (number of bytes). As the memory needed to be allocated gets larger than 64 kb this is going to be doing a lot of unnecessary reallocs. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. All There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There's a cap because the memset is relatively expensive. We expect about half of the last vec extension to be unused after the last part of the stream is read. As is, that's 32k of wasted work, but if there was no cap it could be 10s or 100s of megabytes. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Looking at the implementation of reserve() I think there's two separate issues.
-if the exact behavior of reserve is meant to be part of the API, and will be included in the API, then this algorithm as written is more complex than necessary, and can be made equivalent by always reserving 16 when the capacity equals the length. -However, I expect this behavior is not meant to be a guarantee of the API. Therefore, I think this should probably be coded here directly with reserve_exact to make sure the algorithm is always O(number of bytes).
Not so. Let's pretend we start with a vector that has 32k capacity and size, and need to read in 128 megabytes + 1 byte (so we are doing the maximum wasted work for the doubling algorithm). Let's assume that copying (ie a realloc) and memsetting are equally expensive. For the increase by 32k algorithm, we need to copy and memset on (64k + 96k + 128k + 160k + ... + (128 MB - 32k) + (128 MB) + (128 MB + 32k) = 64(1 + 2 + 3 + ... + 2000 + 2001) kilobytes = 64 * (2001 * 2002) / 2 kilobytes ~= 128e6 kilobytes = 128 gigabytes(!) Being O(n^2), this behavior gets even worse for larger amounts of data, and 128 MB is not at all of an unusual amount of data to be working with. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure if I fully understand what you mean. I'm not able to reproduce the numbers I would expect to see if there was indeed a 250x difference in memory writes: https://gist.github.com/sfackler/81f8a7ac614e19f96cc8 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. See point 1. Due to the not-necessarily apparent and undocumented way in which reserve() decides how to actually reserve memory, the current implementation of reserve() and read_to_end() just happen to sync in such a way that the behavior is equivalent to doubling the size of the vector at each step. If you replace reserve() here with reserve_exact() you should see the behavior I am talking about. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You can see it with this example:
If it were allocating 64k at a time, it would print 1024 * 1024 + 64 * 1024 = 114112. Instead it prints 1024 * 1024 * 2 = 2097152. This is why I think the doubling behavior should be made explicit with reserve_exact(), this is not at all obvious behavior and not mentioned or guaranteed by the API. |
||
cap_bump *= 2; | ||
} | ||
buf.reserve(cap_bump); | ||
} | ||
let new_area = buf.capacity() - buf.len(); | ||
buf.extend(iter::repeat(0).take(new_area)); | ||
} | ||
match with_end_to_cap(buf, |b| r.read(b)) { | ||
Ok(0) => return Ok(read), | ||
Ok(n) => read += n, | ||
|
||
match r.read(&mut buf[len..]) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I realize now that if this panics the length of There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Note that There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, I thought about having a guard fix the length but it seems not worth it. Worst case you see some extra 0 bytes. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. agreed |
||
Ok(0) => { | ||
ret = Ok(len - start_len); | ||
break; | ||
} | ||
Ok(n) => len += n, | ||
Err(ref e) if e.kind() == ErrorKind::Interrupted => {} | ||
Err(e) => return Err(e), | ||
Err(e) => { | ||
ret = Err(e); | ||
break; | ||
} | ||
} | ||
} | ||
|
||
buf.truncate(len); | ||
ret | ||
} | ||
|
||
/// A trait for objects which are byte-oriented sources. | ||
|
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.
I personally tried to keep this to a
Cursor<Vec<u8>>
because it just felt like it was duplicating the functionality otherwise, but I think it would otherwise require unsafe calls toset_len
which is somewhat worrisome.I think that the
buf
could be represented asBox<[u8]>
as well (as the capacity isn't necessary, just the length), but I also think it's fine for now.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 I tried to think of ways to keep it as a
Cursor
but nothing really seemed right, especially sinceCursor<Vec<u8>>
extends the vec on its own.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 I'm fine by this :)