Change:
I was using std::io::copy from an SSD to memory using , which reported an eye-raising 30 MB/s. Wrapping it in a BufReader (either as the outer or middle layer) made no real difference. So I googled around a bit and found the recommended size was 64KB according to some random Microsoft article, so I decided to copy-paste the std::io::copy implementation and increase the buffer size. This got up to 600 MB/s at about 256KB which sounded a lot more it continued going upwards from there past the point of believability (unless the files were being pulled from RAM as a consequence of my repeated accesses). With a 10MB Vec, I got 228.36 GB/s . Unrealistic values aside, has anybody checked the buffer value to make sure there isn't some huge low-hanging fruit for std::io::copy speeds?
You mention windows. This may be platform-specific behavior. On unix systems I would fadvise on the input file for copying. Googling tells me that the windows equivalent is . Have you tried that?
After a coarse overview, my intuition is that there is some funny business going on with the length determined by and the subsequent calls on the , that could explain the unrealistic results.
It’s possible. The actual times did increase dramatically from the std::io::copy default though - my suspicion is that there is probably some caching to RAM going on or something as cold runs were generally slower than hot runs. This was for some work code that I’m no longer actively developing. I’ll leave this email marked as unread for awhile in case I have some spare time to write a benchmark. I don’t think the other flag that was mentioned is accessible with the default open file api, but in any case it at least appears there’s some low hanging fruit wrt to std::io::copy perf
I tried benchmarking on Windoze before and after changing to (instead of the original ) and the results were slightly worse (up to ~3%) with the modified setup, both on HDD and SSD. However, I'm not sure if my benchmarks were good enough - I was having a hard time tweaking them so that I could see any difference between results for different file sizes. FWIW, I'm providing the benchmark code below; it can easily be expanded to different file sizes:
4MB isn’t a whole lot. That’s well within the size of hard drive caches or possibly an OS memory-based cache. I was noticing the difference with files several gigabytes in size. As a sanity check, maybe look to see what the actual time is, not just the % difference between timings with different buffer sizes. The actual time may be wildly different than the actual steady state performance of the disk if it’s reading the data from a cache. It may also be possible at a low level to pass a flag to encourage the OS to disable or flush the cache, but I don’t know if there’s any way to instruct the hard drive to do the same.
I only pasted the benchmark for 4MB as a reference - I tested files from 4KB up to 16MB. I'm not an expert with low-level hard drive stuff, just hoping my attempt can help someone else.
Ideally you would test with a file approximately twice as big as you have RAM, so if you have 8GB of RAM you would want to test with a 16GB file.
extended open flags are accessible via FWIW lots of bennchmarks exist showing synchronous IO calls improve when matching IO operations to the underlying block size (e.g. 4MB for modern SSD's) across multiple OS's, so its very likely that there are improvements to gain here. However, 4MB is stack-breaking size by default, and heap allocations will destroy potential improvements (and further, if we're optimising, we probably want overlapped IO and a ring of buffers rather than just one buffer which will always leave one device idle).... so its complex;)
heap allocations will destroy potential improvements How come? would allocate once at the beginning. Heap memory isn't inherently slower and one memory allocation is basically free compared to the heavy IO that's happening afterwards.
But we also don't want to be allocating 4MB every time we do as someone might be doing a bunch of small copies.
possible approaches: thread local buffers or a buffer pool stat the input before deciding on the buffer size (this is what gnu cp does I think) heap-allocate a bigger buffer after a few iterations of the copy loop with an on-stack buffer specialize copy operations from file to memory to directly into the already allocated target, thus skipping the need for a buffer
specialize copy operations from file to memory to directly into the already allocated target, thus skipping the need for a buffer I think some kind of specialization for sources would be ideal. Because in this case, should not buffer at all and simply use the reader's buffer. Effectively this also gives you a customizable buffer size by wrapping your in a with whatever size you want. As of today, if your source is then is always the wrong answer. I got a substantial performance improvement using a simple loop instead:
a single heap allocation is entirely reasonable for a single invocation of copy of a lot of data. For many copies of a small amount of data, one ends up with a lot of allocations again (consider for instance copying data out of a tar, where we have views on the data and there's no actual synchronous IO etc happening. I like some of the suggested possibilities with specialisation.
It's a nice general optimization, but not necesarily ideal if you use a as . Adding a on the reader size does let you control the buffer size but it still introduces an unnecessary memcopy compared to . Perhaps a writer-side optimization would be more flexible. Then you can specialize either for or
That sounds rather uncommon to me though. After all, is provided by the trait so it's always available. The only case I can see where you would invoke with a as destination is some generic interfaces that takes any . But IMO such interfaces are ill-designed, as they could simply implement instead and offer a much more flexible API.
yes, generic code taking a write was the concern. E.g. tar-rs has a tar archive which takes a and is fed with items. I'm not saying that creating an in-memory archive would be a good idea, just pointing out that these patterns do exist.
I had same kind of issue but with different library (). The copy rate was around 70 MB/s (inside single NVMe SSD). The problem was actually with running the debug version of my program. When I built and ran the release version, the speed jumped to over 1 GB/s.
With merged you can now (on next nightly) use to control the chunk size of . Since this relies on specialization it only works if you pass a directly, any other wrapper around it would disable this optimization.

Discussion:
/// "successfully written" (by returning nonzero success values from /// `write`), any 0-length writes from `inner` must be reported as i/o /// errors from this method. <del> pub(super) fn flush_buf(&mut self) -> io::Result<()> { </del> <ins> pub(in crate::io) fn flush_buf(&mut self) -> io::Result<()> { </ins> /// Helper struct to ensure the buffer is updated after all the writes /// are complete. It tracks the number of written bytes and drains them /// all from the front of the buffer when dropped.

&self.buf } <ins> /// Returns a mutable reference to the internal buffer. /// /// This can be used to write data directly into the buffer without triggering writers /// to the underlying writer. /// /// That the buffer is a `Vec` is an implementation detail. /// Callers should not modify the capacity as there currently is no public API to do so /// and thus any capacity changes would be unexpected by the user. pub(in crate::io) fn buffer_mut(&mut self) -> &mut Vec<u8> { &mut self.buf } </ins> /// Returns the number of bytes the internal buffer can hold without flushing. /// /// # Examples

<del> use crate::io::{self, ErrorKind, Read, Write}; </del> <ins> use super::{BufWriter, ErrorKind, Read, Result, Write, DEFAULT_BUF_SIZE}; </ins> use crate::mem::MaybeUninit; /// Copies the entire contents of a reader into a writer.

/// } /// ``` #[stable(feature = "rust1", since = "1.0.0")] <del> pub fn copy<R: ?Sized, W: ?Sized>(reader: &mut R, writer: &mut W) -> io::Result<u64> </del> <ins> pub fn copy<R: ?Sized, W: ?Sized>(reader: &mut R, writer: &mut W) -> Result<u64> </ins> where R: Read, W: Write,

} } <del> /// The general read-write-loop implementation of /// `io::copy` that is used when specializations are not available or not applicable. pub(crate) fn generic_copy<R: ?Sized, W: ?Sized>(reader: &mut R, writer: &mut W) -> io::Result<u64> </del> <ins> /// The userspace read-write-loop implementation of `io::copy` that is used when /// OS-specific specializations for copy offloading are not available or not applicable. pub(crate) fn generic_copy<R: ?Sized, W: ?Sized>(reader: &mut R, writer: &mut W) -> Result<u64> </ins> where R: Read, W: Write, { <del> let mut buf = MaybeUninit::<[u8; super::DEFAULT_BUF_SIZE]>::uninit(); </del> <ins> BufferedCopySpec::copy_to(reader, writer) } /// Specialization of the read-write loop that either uses a stack buffer /// or reuses the internal buffer of a BufWriter trait BufferedCopySpec: Write { fn copy_to<R: Read + ?Sized>(reader: &mut R, writer: &mut Self) -> Result<u64>; } impl<W: Write + ?Sized> BufferedCopySpec for W { default fn copy_to<R: Read + ?Sized>(reader: &mut R, writer: &mut Self) -> Result<u64> { stack_buffer_copy(reader, writer) } } impl<I: Write> BufferedCopySpec for BufWriter<I> { fn copy_to<R: Read + ?Sized>(reader: &mut R, writer: &mut Self) -> Result<u64> { if writer.capacity() < DEFAULT_BUF_SIZE { return stack_buffer_copy(reader, writer); } // FIXME: #42788 // //   - This creates a (mut) reference to a slice of //     _uninitialized_ integers, which is **undefined behavior** // //   - Only the standard library gets to soundly "ignore" this, //     based on its privileged knowledge of unstable rustc //     internals; unsafe { let spare_cap = writer.buffer_mut().spare_capacity_mut(); reader.initializer().initialize(MaybeUninit::slice_assume_init_mut(spare_cap)); } let mut len = 0; loop { let buf = writer.buffer_mut(); let spare_cap = buf.spare_capacity_mut(); if spare_cap.len() >= DEFAULT_BUF_SIZE { match reader.read(unsafe { MaybeUninit::slice_assume_init_mut(spare_cap) }) { Ok(0) => return Ok(len), // EOF reached Ok(bytes_read) => { assert!(bytes_read <= spare_cap.len()); // Safety: The initializer contract guarantees that either it or `read` // will have initialized these bytes. And we just checked that the number // of bytes is within the buffer capacity. unsafe { buf.set_len(buf.len() + bytes_read) }; len += bytes_read as u64; // Read again if the buffer still has enough capacity, as BufWriter itself would do // This will occur if the reader returns short reads continue; } Err(ref e) if e.kind() == ErrorKind::Interrupted => continue, Err(e) => return Err(e), } } writer.flush_buf()?; } } } fn stack_buffer_copy<R: Read + ?Sized, W: Write + ?Sized>( reader: &mut R, writer: &mut W, ) -> Result<u64> { let mut buf = MaybeUninit::<[u8; DEFAULT_BUF_SIZE]>::uninit(); </ins> // FIXME: #42788 // //   - This creates a (mut) reference to a slice of

<ins> use crate::cmp::{max, min}; </ins> use crate::io::prelude::*; <del> use crate::io::{copy, empty, repeat, sink, Empty, Repeat, SeekFrom, Sink}; </del> <ins> use crate::io::{ copy, empty, repeat, sink, BufWriter, Empty, Repeat, Result, SeekFrom, Sink, DEFAULT_BUF_SIZE, }; </ins> #[test] fn copy_copies() {

assert_eq!(copy(&mut r as &mut dyn Read, &mut w as &mut dyn Write).unwrap(), 1 << 17); } <ins> struct ShortReader { cap: usize, read_size: usize, observed_buffer: usize, } impl Read for ShortReader { fn read(&mut self, buf: &mut [u8]) -> Result<usize> { let bytes = min(self.cap, self.read_size); self.cap -= bytes; self.observed_buffer = max(self.observed_buffer, buf.len()); Ok(bytes) } } struct WriteObserver { observed_buffer: usize, } impl Write for WriteObserver { fn write(&mut self, buf: &[u8]) -> Result<usize> { self.observed_buffer = max(self.observed_buffer, buf.len()); Ok(buf.len()) } fn flush(&mut self) -> Result<()> { Ok(()) } } #[test] fn copy_specializes_bufwriter() { let cap = 117 * 1024; let buf_sz = 16 * 1024; let mut r = ShortReader { cap, observed_buffer: 0, read_size: 1337 }; let mut w = BufWriter::with_capacity(buf_sz, WriteObserver { observed_buffer: 0 }); assert_eq!( copy(&mut r, &mut w).unwrap(), cap as u64, "expected the whole capacity to be copied" ); assert_eq!(r.observed_buffer, buf_sz, "expected a large buffer to be provided to the reader"); assert!(w.get_mut().observed_buffer > DEFAULT_BUF_SIZE, "expected coalesced writes"); } </ins> #[test] fn sink_sinks() { let mut s = sink();

#![feature(maybe_uninit_extra)] #![feature(maybe_uninit_ref)] #![feature(maybe_uninit_slice)] <ins> #![feature(maybe_uninit_uninit_array)] </ins> #![feature(min_specialization)] #![feature(needs_panic_runtime)] #![feature(negative_impls)]

#![feature(unsafe_cell_raw_get)] #![feature(unwind_attributes)] #![feature(vec_into_raw_parts)] <ins> #![feature(vec_spare_capacity)] </ins> #![feature(wake_trait)] // NB: the above list is sorted to minimize merge conflicts. #![default_lib_allocator]