From f2b31009d4e9d22fdb4c08538a554118546e0571 Mon Sep 17 00:00:00 2001 From: Mahdi Ali-Raihan Date: Sat, 18 Jul 2026 18:47:57 -0400 Subject: [PATCH 1/3] avoid re-initializing the spare buffer if we initialized >PROBE_SIZE unfilled bytes in a previous loop --- library/alloc/src/io/read.rs | 39 +++++++++++++++++++++++++++--------- 1 file changed, 30 insertions(+), 9 deletions(-) diff --git a/library/alloc/src/io/read.rs b/library/alloc/src/io/read.rs index c6b802ef9862a..bd09c40e15d61 100644 --- a/library/alloc/src/io/read.rs +++ b/library/alloc/src/io/read.rs @@ -1,4 +1,3 @@ -use core::cmp; use core::mem::{DropGuard, MaybeUninit}; use crate::io::{ @@ -822,8 +821,9 @@ where /// - avoid allocating unless necessary /// - avoid overallocating if we know the exact size (#89165) /// - avoid passing large buffers to readers that always initialize the free capacity if they perform short reads (#23815, #23820) +/// - avoid re-initializing the spare buffer if we initialized >PROBE_SIZE unfilled bytes in a previous loop (#158008) /// - pass large buffers to readers that do not initialize the spare capacity. this can amortize per-call overheads -/// - and finally pass not-too-small and not-too-large buffers to Windows read APIs because they manage to suffer from both problems +/// - pass not-too-small and not-too-large buffers to Windows read APIs because they manage to suffer from both problems /// at the same time, i.e. small reads suffer from syscall overhead, all reads incur costs proportional to buffer size (#110650) #[doc(hidden)] #[unstable(feature = "core_io_internals", reason = "exposed only for libstd", issue = "none")] @@ -840,6 +840,9 @@ pub fn default_read_to_end( .and_then(|s| s.checked_add(1024)?.checked_next_multiple_of(DEFAULT_BUF_SIZE)) .unwrap_or(DEFAULT_BUF_SIZE); + // Tracks how many bytes are initialized in the buffer + let mut init_until = buf.len(); + const PROBE_SIZE: usize = 32; fn small_probe_read(r: &mut R, buf: &mut Vec) -> Result { @@ -893,10 +896,10 @@ pub fn default_read_to_end( // unnecessary doubling of the capacity. But if not, append the // probe buffer to the primary buffer and let its capacity grow. let read = small_probe_read(r, buf)?; - if read == 0 { return Ok(buf.len() - start_len); } + init_until = buf.len(); } if buf.len() == buf.capacity() { @@ -904,13 +907,25 @@ pub fn default_read_to_end( buf.try_reserve(PROBE_SIZE)?; } + // We set a threshold of >PROBE_SIZE initialized yet unfilled bytes left in the + // spare buffer before determining that we need to initialize more bytes into + // the spare buffer + 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; + let mut spare = buf.spare_capacity_mut(); - let buf_len = cmp::min(spare.len(), max_read_size); spare = &mut spare[..buf_len]; let mut read_buf: BorrowedBuf<'_, u8> = spare.into(); - // Note that we don't track already initialized bytes here, but this is fine - // because we explicitly limit the read size + if was_init { + // SAFETY: These bytes were initialized but not filled in the previous loop + unsafe { read_buf.set_init() }; + } + let mut cursor = read_buf.unfilled(); let result = loop { match r.read_buf(cursor.reborrow()) { @@ -924,6 +939,10 @@ pub fn default_read_to_end( let bytes_read = cursor.written(); let is_init = read_buf.is_init(); + if is_init { + init_until = buf.len() + buf_len; + } + // SAFETY: BorrowedBuf's invariants mean this much memory is initialized. unsafe { let new_len = bytes_read + buf.len(); @@ -948,9 +967,11 @@ pub fn default_read_to_end( if !is_init { max_read_size = usize::MAX; } - // we have passed a larger buffer than previously and the - // reader still hasn't returned a short read - else if buf_len >= max_read_size && bytes_read == buf_len { + // the spare buffer has initialized and read in `max_read_size` bytes. + // it's possible that we have more than `max_read_size` bytes to read + // left, so a larger buffer may be necessary to minimize the number of + // iterations of reading in bytes to the buffer + else if bytes_read == max_read_size { max_read_size = max_read_size.saturating_mul(2); } } From 9ebbabccf402793835de2a46976f6c6a9fea8b78 Mon Sep 17 00:00:00 2001 From: Chris Denton Date: Sun, 9 Aug 2026 13:00:02 -0400 Subject: [PATCH 2/3] Avoid unnecessarily short reads in `read_to_end` On Windows, the UTF-16 to UTF-8 translation is made simpler by ensuring we don't split code points.: --- library/alloc/src/io/read.rs | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/library/alloc/src/io/read.rs b/library/alloc/src/io/read.rs index bd09c40e15d61..72e73abe3d6c1 100644 --- a/library/alloc/src/io/read.rs +++ b/library/alloc/src/io/read.rs @@ -821,10 +821,11 @@ where /// - avoid allocating unless necessary /// - avoid overallocating if we know the exact size (#89165) /// - avoid passing large buffers to readers that always initialize the free capacity if they perform short reads (#23815, #23820) -/// - avoid re-initializing the spare buffer if we initialized >PROBE_SIZE unfilled bytes in a previous loop (#158008) +/// - avoid re-initializing unfilled bytes into the spare buffer if we initialized >PROBE_SIZE unfilled bytes in a previous loop (#158008) /// - pass large buffers to readers that do not initialize the spare capacity. this can amortize per-call overheads /// - pass not-too-small and not-too-large buffers to Windows read APIs because they manage to suffer from both problems /// at the same time, i.e. small reads suffer from syscall overhead, all reads incur costs proportional to buffer size (#110650) +/// - also avoid <4 byte reads as this may split UTF-8 code points, which can be a problem for Windows console reads (#142847) #[doc(hidden)] #[unstable(feature = "core_io_internals", reason = "exposed only for libstd", issue = "none")] pub fn default_read_to_end( @@ -890,22 +891,27 @@ pub fn default_read_to_end( } loop { - if buf.len() == buf.capacity() && buf.capacity() == start_cap { + if buf.spare_capacity_mut().len() < PROBE_SIZE && buf.capacity() == start_cap { // The buffer might be an exact fit. Let's read into a probe buffer // and see if it returns `Ok(0)`. If so, we've avoided an // unnecessary doubling of the capacity. But if not, append the // probe buffer to the primary buffer and let its capacity grow. let read = small_probe_read(r, buf)?; + if read == 0 { return Ok(buf.len() - start_len); } + init_until = buf.len(); + // In the case of very short reads, continue to use the stack buffer + // until either we reach the end or we need to reallocate. + continue; } - if buf.len() == buf.capacity() { - // buf is full, need more space - buf.try_reserve(PROBE_SIZE)?; - } + // Avoid unnecessarily short reads by ensuring there's at least PROBE_SIZE space available. + // And assert that PROBE_SIZE is always at least large enough to fit any UTF-8 encoded code point. + const { assert!(PROBE_SIZE >= char::MAX_LEN_UTF8) } + buf.try_reserve(PROBE_SIZE)?; // We set a threshold of >PROBE_SIZE initialized yet unfilled bytes left in the // spare buffer before determining that we need to initialize more bytes into From 9e13b0a69e35a0ec5ea92c9ab0e5cb5ba9fac530 Mon Sep 17 00:00:00 2001 From: Mahdi Ali-Raihan Date: Mon, 10 Aug 2026 12:28:08 -0400 Subject: [PATCH 3/3] Re-calibrate init_until to how many bytes are actually initialized and filled in the buffer when reallocation occurs --- library/alloc/src/io/read.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/library/alloc/src/io/read.rs b/library/alloc/src/io/read.rs index 72e73abe3d6c1..6ca8ff6319eff 100644 --- a/library/alloc/src/io/read.rs +++ b/library/alloc/src/io/read.rs @@ -911,7 +911,12 @@ pub fn default_read_to_end( // Avoid unnecessarily short reads by ensuring there's at least PROBE_SIZE space available. // And assert that PROBE_SIZE is always at least large enough to fit any UTF-8 encoded code point. const { assert!(PROBE_SIZE >= char::MAX_LEN_UTF8) } - buf.try_reserve(PROBE_SIZE)?; + if buf.spare_capacity_mut().len() < PROBE_SIZE { + buf.try_reserve(PROBE_SIZE)?; + // When reallocation occurs, we have to update init_until accordingly + // to re-calibrate how many bytes are actually initialized in the buffer + init_until = buf.len(); + } // We set a threshold of >PROBE_SIZE initialized yet unfilled bytes left in the // spare buffer before determining that we need to initialize more bytes into