Skip to content

Fix perf regression in Read::read_to_end on short reads due to not checking if the cursor has initialized bytes - #158083

Open
asder8215 wants to merge 1 commit into
rust-lang:mainfrom
asder8215:default_read_to_end_mark_init
Open

Fix perf regression in Read::read_to_end on short reads due to not checking if the cursor has initialized bytes#158083
asder8215 wants to merge 1 commit into
rust-lang:mainfrom
asder8215:default_read_to_end_mark_init

Conversation

@asder8215

@asder8215 asder8215 commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

View all comments

This PR fixes #158008.

In particular, in #150129, it refactored some code within library/std/io/mod.rs to utilize BorrowedBuf::is_init instead of manually checking read_buf.init_len() == buf_len to see if the read buffer had initialized bytes. However, the BorrowedBuf is never marked or set as init within this function, and I think this portion of the code:

 // SAFETY: These bytes were initialized but not filled in the previous loop
unsafe {
     read_buf.set_init(initialized);
}

was removed by mistake. This PR reverts the changes made by #150129, so that we can mark the BorrowedBuf/read_buf as initialized using BorrowedBuf::set_init if in a previous iteration the cursor has initialized bytes. This would allow max_read_size to not be marked as usize::max if the read buffer contains initialized bytes.

@rustbot rustbot added S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. T-libs Relevant to the library team, which will review and decide on the PR/issue. labels Jun 18, 2026
@rustbot

rustbot commented Jun 18, 2026

Copy link
Copy Markdown
Collaborator

r? @Darksonn

rustbot has assigned @Darksonn.
They will have a look at your PR within the next two weeks and either review your PR or reassign to another reviewer.

Use r? to explicitly pick a reviewer

Why was this reviewer chosen?

The reviewer was selected based on:

  • Owners of files modified in this PR: @ChrisDenton, libs
  • @ChrisDenton, libs expanded to 12 candidates
  • Random selection from Darksonn, Mark-Simulacrum, clarfonthey, jhpratt

@asder8215 asder8215 changed the title Fix perf regression in Read::read_to_end on short reads due to not checking if the cursor has initialized bytes Fix perf regression in Read::read_to_end on short reads due to not checking if the cursor has initialized bytes Jun 18, 2026
Comment thread library/std/src/io/mod.rs Outdated
Comment on lines 484 to 485
// Note that we don't track already initialized bytes here, but this is fine
// because we explicitly limit the read size

@Darksonn Darksonn Jun 18, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment was added in #150129. Should it be removed?

View changes since the review

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unsure. Are these comments still relevant @a1phyr?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, probably not if you start tracking initialized bytes :)

Comment thread library/std/src/io/mod.rs Outdated
Comment thread library/std/src/io/mod.rs Outdated
Comment thread library/std/src/io/mod.rs Outdated
@Darksonn

Copy link
Copy Markdown
Member

@rustbot author

@rustbot rustbot added S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. and removed S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. labels Jun 18, 2026
@rustbot

rustbot commented Jun 18, 2026

Copy link
Copy Markdown
Collaborator

Reminder, once the PR becomes ready for a review, use @rustbot ready.

@Darksonn Darksonn added the A-io Area: `std::io`, `std::fs`, `std::net` and `std::path` label Jun 18, 2026
@asder8215
asder8215 force-pushed the default_read_to_end_mark_init branch from 104baaa to aebb8e1 Compare June 18, 2026 16:03
@asder8215
asder8215 requested a review from Darksonn June 18, 2026 16:05
@rustbot rustbot added S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. and removed S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. labels Jun 18, 2026
@asder8215
asder8215 force-pushed the default_read_to_end_mark_init branch from aebb8e1 to c6406c5 Compare June 18, 2026 16:06

@a1phyr a1phyr left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

View changes since this review

Comment thread library/std/src/io/mod.rs Outdated
}
};

initialized_len = cursor.capacity();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is true only if read_buf.is_init() (use the boolean below to avoid lifetime issues)

Comment thread library/std/src/io/mod.rs Outdated
let mut read_buf: BorrowedBuf<'_, u8> = spare.into();

let buf_unfilled_len = read_buf.capacity() - read_buf.len();
if initialized_len == buf_unfilled_len {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, please store the full capacity of the vector, including anything already written.

@Darksonn

Copy link
Copy Markdown
Member

@rustbot author

@rustbot rustbot added S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. and removed S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. labels Jun 20, 2026
@asder8215
asder8215 force-pushed the default_read_to_end_mark_init branch from c6406c5 to c0189d9 Compare July 5, 2026 06:38
@asder8215
asder8215 requested a review from a1phyr July 5, 2026 06:39
@asder8215

Copy link
Copy Markdown
Contributor Author

@rustbot ready

@rustbot rustbot added S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. and removed S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. labels Jul 5, 2026
@Darksonn

Darksonn commented Jul 6, 2026

Copy link
Copy Markdown
Member

This is still using the wrong condition. Consider this scenario:

  1. We set initialized_len = 10 - 5 where capacity=10, length=5.
  2. The vector is resized and the capacity is now 15.
  3. We read another 5 bytes with is_init=false.
  4. The next iteration compares initialized_len with 15-10 which is true, and considers the buffer initialized.

But in this scenario the buffer was resized and not initialized, so this is wrong. It's really important that initialized_len is such that after the vector is resized, the comparison can never return true until you get a read with is_init=true.

Instead of storing capacity() - len() please just store capacity(). That way, if the capacity changes, then the capacity will no longer be the same.

@rustbot author

@rustbot rustbot added S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. and removed S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. labels Jul 6, 2026
@asder8215

Copy link
Copy Markdown
Contributor Author

Instead of storing capacity() - len() please just store capacity(). That way, if the capacity changes, then the capacity will no longer be the same.

Just to clarify, resizing only occurs potentially in spots where we call small_probe_reads or in the conditional where buf.len() == buf.capacity() and it does a try_reserve? It should still be safe to use buf_unfilled_len = capacity() - len() on the portion of code where we're converting the spare portion of the buffer into a BorrowedBuf/BorrowedCursor as it shouldn't be able to resize this borrowed buffer/slice with read_buf?

@Darksonn

Darksonn commented Jul 6, 2026

Copy link
Copy Markdown
Member

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 set_init().

@rustbot

This comment has been minimized.

@asder8215
asder8215 force-pushed the default_read_to_end_mark_init branch from c4bc8ab to d0ef9b7 Compare July 7, 2026 21:51
@rust-log-analyzer

This comment has been minimized.

@asder8215
asder8215 force-pushed the default_read_to_end_mark_init branch from d0ef9b7 to 603ccae Compare July 7, 2026 22:45
@asder8215

Copy link
Copy Markdown
Contributor Author

@rustbot ready

@rustbot rustbot added S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. and removed S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. labels Jul 9, 2026
Comment thread library/std/src/io/mod.rs Outdated
Comment on lines +474 to +482
} else {
written_bytes = 0;
}
}

if buf.len() == buf.capacity() {
// buf is full, need more space
buf.try_reserve(PROBE_SIZE)?;
written_bytes = 0;

@Darksonn Darksonn Jul 8, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be less confusing to also set is_init to false here.

View changes since the review

Comment thread library/std/src/io/mod.rs Outdated
Comment on lines +428 to +431
// 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;

@Darksonn Darksonn Jul 8, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

View changes since the review

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

@Darksonn Darksonn added S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. and removed S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. labels Jul 9, 2026
@rust-bors

This comment has been minimized.

@asder8215
asder8215 force-pushed the default_read_to_end_mark_init branch from 603ccae to b28eb14 Compare July 18, 2026 22:48
@rustbot

rustbot commented Jul 18, 2026

Copy link
Copy Markdown
Collaborator

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.

@asder8215
asder8215 requested a review from Darksonn July 19, 2026 00:35
@rustbot rustbot added S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. and removed S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. labels Jul 19, 2026
Comment thread library/alloc/src/io/read.rs Outdated
Comment on lines 985 to 989

@Darksonn Darksonn Aug 5, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe you have to set is_init to false here too? Since this may increase the number of bytes passed in the next iteration.

View changes since the review

@asder8215 asder8215 Aug 5, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't is_init get set to false in the next loop? I believe this conditional doubles max_read_size when it notices that we filled the spare buffer with max_read_size bytes in one iteration rather than through multiple iterations. From the next loop, I think the buf.len() == buf.capacity() conditional earlier should be triggered, resize the buffer, and set is_init to false.

Also, now that I realize this, I think it would be impossible for buf_len > max_read_size because we already accounted for re-using any remaining uninitialized space in the spare buffer rather than creating more uninitialized bytes. I could change this conditional to just bytes_read == max_read_size?

@rustbot rustbot added S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. and removed S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. labels Aug 5, 2026
@asder8215
asder8215 force-pushed the default_read_to_end_mark_init branch 2 times, most recently from 66dba61 to 660f9c5 Compare August 6, 2026 07:36

@asder8215 asder8215 Aug 6, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This conditional right here (which is effectively buf.len() == start_cap due to transitivity) only seems to be ran once to check if the buffer's length is the same as the starting buffer capacity (the ideal case here is that small_probe_read returns 0 and hence we can say for sure that whatever we read into the buffer was an exact fit + avoided doubling the buffer's capacity). Otherwise, if there were more bytes to read, then our buffer's capacity grows and it should be impossible for buf.len() == start_cap (start_cap does not change); therefore, I took this out of the loop since we don't need to run through this multiple times.

View changes since the review

Comment thread library/alloc/src/io/read.rs
@asder8215
asder8215 force-pushed the default_read_to_end_mark_init branch from 660f9c5 to 4cfc77b Compare August 7, 2026 07:13
}

let (was_init, buf_len) = if init_until > buf.len() {

@asder8215 asder8215 Aug 7, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still need was_init to set BorrowedBuf init field to true, and I use it down below so that we're not repeatedly assigning init_until with the same value unless we reach a point where we need to initialize more bytes into the spare buffer.

View changes since the review

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here's a thought: if init_until - buf.len() is positive but very small, we probably want to increase the buffer size even if was_init becomes false. Previously PROBE_SIZE was used a threshold for this. How about doing this?

let buf_len = if init_until > buf.len() + PROBE_SIZE {
    init_until - buf.len()
} else {
    usize::min(max_read_size, buf.capacity() - buf.len())
};
let was_init = init_until >= buf.len() + buf_len;

Comment thread library/alloc/src/io/read.rs
Comment thread library/alloc/src/io/read.rs Outdated
…ecking if the cursor has initialized bytes and refactored main loop code
@asder8215
asder8215 force-pushed the default_read_to_end_mark_init branch from 4cfc77b to e8987d5 Compare August 7, 2026 22:24
@@ -892,30 +889,38 @@ pub fn default_read_to_end<R: Read + ?Sized>(
}

loop {
if buf.len() == buf.capacity() && buf.capacity() == start_cap {
if buf.len() == buf.capacity() {

@asder8215 asder8215 Aug 7, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Kept the exact fit check inside the buf.len() == buf.capacity() conditional, so that we're not running through both conditional separately/repeatedly in the case that buf.len() != buf.capacity(). Hope that's fine.

View changes since the review

@Darksonn Darksonn Aug 8, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the check has to be done twice. When small_probe_read calls extend_from_slice, that will reallocate and then push more bytes. Since more bytes are pushed after the reallocation, there is a possible scenario where the reallocation increases the size by the same amount read, which leads to buf.len() == buf.capacity() being the case after the call to small_probe_read.

Therefore, you must check buf.len() == buf.capacity() again after calling small_probe_read. Otherwise you will call read_buf on an empty slice below, which doesn't work.

@asder8215 asder8215 Aug 8, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right and in that case you'll end up having bytes_read = 0, which will cause an early return even though there may still be more bytes to read.

@ChrisDenton

Copy link
Copy Markdown
Member

I think this might have some overlap with #142872 (that PR is a year old but I was just reminded of it)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-io Area: `std::io`, `std::fs`, `std::net` and `std::path` S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. T-libs Relevant to the library team, which will review and decide on the PR/issue.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Read::read_to_end performance regression

6 participants