Fix perf regression in Read::read_to_end on short reads due to not checking if the cursor has initialized bytes#158083
Conversation
|
r? @Darksonn rustbot has assigned @Darksonn. Use Why was this reviewer chosen?The reviewer was selected based on:
|
Read::read_to_end on short reads due to not checking if the cursor has initialized bytes
| // Note that we don't track already initialized bytes here, but this is fine | ||
| // because we explicitly limit the read size |
There was a problem hiding this comment.
This comment was added in #150129. Should it be removed?
There was a problem hiding this comment.
Unsure. Are these comments still relevant @a1phyr?
There was a problem hiding this comment.
Well, probably not if you start tracking initialized bytes :)
|
@rustbot author |
|
Reminder, once the PR becomes ready for a review, use |
104baaa to
aebb8e1
Compare
aebb8e1 to
c6406c5
Compare
There was a problem hiding this comment.
I didn't track uninitialized bytes in my previous MR because I thought it would be to complicated to do properly.
For example, if this MR improve some existing cases, it will not really solve the pathological case you sent in your issue for larger sizes (eg around a million): when you initialized N bytes, on the next round you will have N-1 spare initialized bytes left, so you won't be able to use set_init() (or you could initialize the rest manually).
All in all, it was a trade-off between code complexity, properly handling common cases but having suboptimal (but still acceptable) behavior in weird cases.
| } | ||
| }; | ||
|
|
||
| initialized_len = cursor.capacity(); |
There was a problem hiding this comment.
This is true only if read_buf.is_init() (use the boolean below to avoid lifetime issues)
| let mut read_buf: BorrowedBuf<'_, u8> = spare.into(); | ||
|
|
||
| let buf_unfilled_len = read_buf.capacity() - read_buf.len(); | ||
| if initialized_len == buf_unfilled_len { |
There was a problem hiding this comment.
This condition is wrong: you compare the old buffer capacity and the new buffer spare capacity, but the start of the buffer has changed since then. It would be less error prone to track initialized bytes counting from the beginning of the Vec.
There was a problem hiding this comment.
Yeah, please store the full capacity of the vector, including anything already written.
|
@rustbot author |
c6406c5 to
c0189d9
Compare
|
@rustbot ready |
|
This is still using the wrong condition. Consider this scenario:
But in this scenario the buffer was resized and not initialized, so this is wrong. It's really important that Instead of storing @rustbot author |
Just to clarify, resizing only occurs potentially in spots where we call |
|
It's not obvious to me that that's the case. Maybe you are right, but I can't easily follow the logic. To me, it would be a lot easier to figure out that the code is correct if you stored the capacity. If the vector was reallocated, then I know that the capacity changed, and therefore I know that we will not call |
|
I just noticed that you added two lines of code to recompute The reason I suggested storing the capacity is that you can delete those two lines of code. I'm worried that, in the future, somebody changes this code to add a third place where the vector is reallocated, while forgetting to also update |
I think it does because it seems like the rust/library/std/src/io/mod.rs Lines 574 to 581 in 36714a9 If I'm understanding it correctly, the The reason why I asked if it was safe to do |
|
I'm sorry it looks like you're right. I misunderstood the However, I don't think the current logic looks ideal. Let's say that we have a buffer of capacity 1000 and
So it seems like that on short reads, we should repeatedly call |
c0189d9 to
c4bc8ab
Compare
This comment has been minimized.
This comment has been minimized.
c4bc8ab to
d0ef9b7
Compare
This comment has been minimized.
This comment has been minimized.
d0ef9b7 to
603ccae
Compare
|
@rustbot ready |
| } else { | ||
| written_bytes = 0; | ||
| } | ||
| } | ||
|
|
||
| if buf.len() == buf.capacity() { | ||
| // buf is full, need more space | ||
| buf.try_reserve(PROBE_SIZE)?; | ||
| written_bytes = 0; |
There was a problem hiding this comment.
I think it would be less confusing to also set is_init to false here.
| // Additively counts how many bytes we wrote into buf in each | ||
| // iteration of the loop; it's used to see if we should initialize | ||
| // more bytes or re-use the initialized buffer space. | ||
| let mut written_bytes = 0; |
There was a problem hiding this comment.
This variable keeps track of what operations we have done in the past, but I think it's generally easier to think about variables that keep track of facts about the world.
So for example, could we store the length of the subset of the buffer that we are currently reading into? Before each iteration you do if buf_len > spare.len() { buf_len = spare.len(); }. And after each iteration you do buf_len -= bytes_read followed by if buf_len == 0 { buf_len = max_read_size; }. The is_init variable then keeps track of whether the current subset is initialized or not.
There was a problem hiding this comment.
I ended up refactoring and using the is_init variable and spare buffer length to determine whether we need to initialize more bytes into the spare buffer or not.
If our spare buffer has a remainder from the modulo operation with max_read_size, it should mean that there are bytes there that we have initialized; otherwise, if we see a 0, then that indicates that we need to initialize more bytes into the spare buffer (aside from the case when our spare buffer is the minimum between itself and the max_read_size in which case taking the mod of that with itself would always produce 0 and anyways we just need to present that remaining spare buffer length).
This comment has been minimized.
This comment has been minimized.
…ecking if the cursor has initialized bytes
603ccae to
b28eb14
Compare
|
This PR was rebased onto a different main commit. Here's a range-diff highlighting what actually changed. Rebasing is a normal part of keeping PRs up to date, so no action is needed—this note is just to help reviewers. |
View all comments
This PR fixes #158008.
In particular, in #150129, it refactored some code within
library/std/io/mod.rsto utilizeBorrowedBuf::is_initinstead of manually checkingread_buf.init_len() == buf_lento see if the read buffer had initialized bytes. However, theBorrowedBufis never marked or set as init within this function, and I think this portion of the code:was removed by mistake. This PR reverts the changes made by #150129, so that we can mark the
BorrowedBuf/read_bufas initialized usingBorrowedBuf::set_initif in a previous iteration the cursor has initialized bytes. This would allowmax_read_sizeto not be marked asusize::maxif the read buffer contains initialized bytes.