Skip to content

Commit 30e49a9

Browse files
committed
Auto merge of #75272 - the8472:spec-copy, r=KodrAus
specialize io::copy to use copy_file_range, splice or sendfile Fixes #74426. Also covers #60689 but only as an optimization instead of an official API. The specialization only covers std-owned structs so it should avoid the problems with #71091 Currently linux-only but it should be generalizable to other unix systems that have sendfile/sosplice and similar. There is a bit of optimization potential around the syscall count. Right now it may end up doing more syscalls than the naive copy loop when doing short (<8KiB) copies between file descriptors. The test case executes the following: ``` [pid 103776] statx(3, "", AT_STATX_SYNC_AS_STAT|AT_EMPTY_PATH, STATX_ALL, {stx_mask=STATX_ALL|STATX_MNT_ID, stx_attributes=0, stx_mode=S_IFREG|0644, stx_size=17, ...}) = 0 [pid 103776] write(4, "wxyz", 4) = 4 [pid 103776] write(4, "iklmn", 5) = 5 [pid 103776] copy_file_range(3, NULL, 4, NULL, 5, 0) = 5 ``` 0-1 `stat` calls to identify the source file type. 0 if the type can be inferred from the struct from which the FD was extracted 𝖬 `write` to drain the `BufReader`/`BufWriter` wrappers. only happen when buffers are present. 𝖬 ≾ number of wrappers present. If there is a write buffer it may absorb the read buffer contents first so only result in a single write. Vectored writes would also be an option but that would require more invasive changes to `BufWriter`. 𝖭 `copy_file_range`/`splice`/`sendfile` until file size, EOF or the byte limit from `Take` is reached. This should generally be *much* more efficient than the read-write loop and also have other benefits such as DMA offload or extent sharing. ## Benchmarks ``` OLD test io::tests::bench_file_to_file_copy ... bench: 21,002 ns/iter (+/- 750) = 6240 MB/s [ext4] test io::tests::bench_file_to_file_copy ... bench: 35,704 ns/iter (+/- 1,108) = 3671 MB/s [btrfs] test io::tests::bench_file_to_socket_copy ... bench: 57,002 ns/iter (+/- 4,205) = 2299 MB/s test io::tests::bench_socket_pipe_socket_copy ... bench: 142,640 ns/iter (+/- 77,851) = 918 MB/s NEW test io::tests::bench_file_to_file_copy ... bench: 14,745 ns/iter (+/- 519) = 8889 MB/s [ext4] test io::tests::bench_file_to_file_copy ... bench: 6,128 ns/iter (+/- 227) = 21389 MB/s [btrfs] test io::tests::bench_file_to_socket_copy ... bench: 13,767 ns/iter (+/- 3,767) = 9520 MB/s test io::tests::bench_socket_pipe_socket_copy ... bench: 26,471 ns/iter (+/- 6,412) = 4951 MB/s ```
2 parents 66c1309 + bbfa92c commit 30e49a9

File tree

11 files changed

+930
-152
lines changed

11 files changed

+930
-152
lines changed

library/std/src/fs.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -1656,7 +1656,7 @@ pub fn rename<P: AsRef<Path>, Q: AsRef<Path>>(from: P, to: Q) -> io::Result<()>
16561656
/// the length of the `to` file as reported by `metadata`.
16571657
///
16581658
/// If you’re wanting to copy the contents of one file to another and you’re
1659-
/// working with [`File`]s, see the [`io::copy`] function.
1659+
/// working with [`File`]s, see the [`io::copy()`] function.
16601660
///
16611661
/// # Platform-specific behavior
16621662
///

library/std/src/io/copy.rs

+88
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
use crate::io::{self, ErrorKind, Read, Write};
2+
use crate::mem::MaybeUninit;
3+
4+
/// Copies the entire contents of a reader into a writer.
5+
///
6+
/// This function will continuously read data from `reader` and then
7+
/// write it into `writer` in a streaming fashion until `reader`
8+
/// returns EOF.
9+
///
10+
/// On success, the total number of bytes that were copied from
11+
/// `reader` to `writer` is returned.
12+
///
13+
/// If you’re wanting to copy the contents of one file to another and you’re
14+
/// working with filesystem paths, see the [`fs::copy`] function.
15+
///
16+
/// [`fs::copy`]: crate::fs::copy
17+
///
18+
/// # Errors
19+
///
20+
/// This function will return an error immediately if any call to [`read`] or
21+
/// [`write`] returns an error. All instances of [`ErrorKind::Interrupted`] are
22+
/// handled by this function and the underlying operation is retried.
23+
///
24+
/// [`read`]: Read::read
25+
/// [`write`]: Write::write
26+
///
27+
/// # Examples
28+
///
29+
/// ```
30+
/// use std::io;
31+
///
32+
/// fn main() -> io::Result<()> {
33+
/// let mut reader: &[u8] = b"hello";
34+
/// let mut writer: Vec<u8> = vec![];
35+
///
36+
/// io::copy(&mut reader, &mut writer)?;
37+
///
38+
/// assert_eq!(&b"hello"[..], &writer[..]);
39+
/// Ok(())
40+
/// }
41+
/// ```
42+
#[stable(feature = "rust1", since = "1.0.0")]
43+
pub fn copy<R: ?Sized, W: ?Sized>(reader: &mut R, writer: &mut W) -> io::Result<u64>
44+
where
45+
R: Read,
46+
W: Write,
47+
{
48+
cfg_if::cfg_if! {
49+
if #[cfg(any(target_os = "linux", target_os = "android"))] {
50+
crate::sys::kernel_copy::copy_spec(reader, writer)
51+
} else {
52+
generic_copy(reader, writer)
53+
}
54+
}
55+
}
56+
57+
/// The general read-write-loop implementation of
58+
/// `io::copy` that is used when specializations are not available or not applicable.
59+
pub(crate) fn generic_copy<R: ?Sized, W: ?Sized>(reader: &mut R, writer: &mut W) -> io::Result<u64>
60+
where
61+
R: Read,
62+
W: Write,
63+
{
64+
let mut buf = MaybeUninit::<[u8; super::DEFAULT_BUF_SIZE]>::uninit();
65+
// FIXME: #42788
66+
//
67+
// - This creates a (mut) reference to a slice of
68+
// _uninitialized_ integers, which is **undefined behavior**
69+
//
70+
// - Only the standard library gets to soundly "ignore" this,
71+
// based on its privileged knowledge of unstable rustc
72+
// internals;
73+
unsafe {
74+
reader.initializer().initialize(buf.assume_init_mut());
75+
}
76+
77+
let mut written = 0;
78+
loop {
79+
let len = match reader.read(unsafe { buf.assume_init_mut() }) {
80+
Ok(0) => return Ok(written),
81+
Ok(len) => len,
82+
Err(ref e) if e.kind() == ErrorKind::Interrupted => continue,
83+
Err(e) => return Err(e),
84+
};
85+
writer.write_all(unsafe { &buf.assume_init_ref()[..len] })?;
86+
written += len as u64;
87+
}
88+
}

library/std/src/io/mod.rs

+4-1
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,8 @@ pub use self::buffered::IntoInnerError;
266266
#[stable(feature = "rust1", since = "1.0.0")]
267267
pub use self::buffered::{BufReader, BufWriter, LineWriter};
268268
#[stable(feature = "rust1", since = "1.0.0")]
269+
pub use self::copy::copy;
270+
#[stable(feature = "rust1", since = "1.0.0")]
269271
pub use self::cursor::Cursor;
270272
#[stable(feature = "rust1", since = "1.0.0")]
271273
pub use self::error::{Error, ErrorKind, Result};
@@ -279,11 +281,12 @@ pub use self::stdio::{_eprint, _print};
279281
#[doc(no_inline, hidden)]
280282
pub use self::stdio::{set_panic, set_print, LocalOutput};
281283
#[stable(feature = "rust1", since = "1.0.0")]
282-
pub use self::util::{copy, empty, repeat, sink, Empty, Repeat, Sink};
284+
pub use self::util::{empty, repeat, sink, Empty, Repeat, Sink};
283285

284286
pub(crate) use self::stdio::clone_io;
285287

286288
mod buffered;
289+
pub(crate) mod copy;
287290
mod cursor;
288291
mod error;
289292
mod impls;

library/std/src/io/stdio.rs

+8
Original file line numberDiff line numberDiff line change
@@ -409,6 +409,14 @@ impl Read for Stdin {
409409
}
410410
}
411411

412+
// only used by platform-dependent io::copy specializations, i.e. unused on some platforms
413+
#[cfg(any(target_os = "linux", target_os = "android"))]
414+
impl StdinLock<'_> {
415+
pub(crate) fn as_mut_buf(&mut self) -> &mut BufReader<impl Read> {
416+
&mut self.inner
417+
}
418+
}
419+
412420
#[stable(feature = "rust1", since = "1.0.0")]
413421
impl Read for StdinLock<'_> {
414422
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {

library/std/src/io/tests.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use super::{repeat, Cursor, SeekFrom};
22
use crate::cmp::{self, min};
3-
use crate::io::prelude::*;
43
use crate::io::{self, IoSlice, IoSliceMut};
4+
use crate::io::{BufRead, Read, Seek, Write};
55
use crate::ops::Deref;
66

77
#[test]

library/std/src/io/util.rs

+1-72
Original file line numberDiff line numberDiff line change
@@ -4,78 +4,7 @@
44
mod tests;
55

66
use crate::fmt;
7-
use crate::io::{self, BufRead, ErrorKind, Initializer, IoSlice, IoSliceMut, Read, Write};
8-
use crate::mem::MaybeUninit;
9-
10-
/// Copies the entire contents of a reader into a writer.
11-
///
12-
/// This function will continuously read data from `reader` and then
13-
/// write it into `writer` in a streaming fashion until `reader`
14-
/// returns EOF.
15-
///
16-
/// On success, the total number of bytes that were copied from
17-
/// `reader` to `writer` is returned.
18-
///
19-
/// If you’re wanting to copy the contents of one file to another and you’re
20-
/// working with filesystem paths, see the [`fs::copy`] function.
21-
///
22-
/// [`fs::copy`]: crate::fs::copy
23-
///
24-
/// # Errors
25-
///
26-
/// This function will return an error immediately if any call to [`read`] or
27-
/// [`write`] returns an error. All instances of [`ErrorKind::Interrupted`] are
28-
/// handled by this function and the underlying operation is retried.
29-
///
30-
/// [`read`]: Read::read
31-
/// [`write`]: Write::write
32-
///
33-
/// # Examples
34-
///
35-
/// ```
36-
/// use std::io;
37-
///
38-
/// fn main() -> io::Result<()> {
39-
/// let mut reader: &[u8] = b"hello";
40-
/// let mut writer: Vec<u8> = vec![];
41-
///
42-
/// io::copy(&mut reader, &mut writer)?;
43-
///
44-
/// assert_eq!(&b"hello"[..], &writer[..]);
45-
/// Ok(())
46-
/// }
47-
/// ```
48-
#[stable(feature = "rust1", since = "1.0.0")]
49-
pub fn copy<R: ?Sized, W: ?Sized>(reader: &mut R, writer: &mut W) -> io::Result<u64>
50-
where
51-
R: Read,
52-
W: Write,
53-
{
54-
let mut buf = MaybeUninit::<[u8; super::DEFAULT_BUF_SIZE]>::uninit();
55-
// FIXME: #42788
56-
//
57-
// - This creates a (mut) reference to a slice of
58-
// _uninitialized_ integers, which is **undefined behavior**
59-
//
60-
// - Only the standard library gets to soundly "ignore" this,
61-
// based on its privileged knowledge of unstable rustc
62-
// internals;
63-
unsafe {
64-
reader.initializer().initialize(buf.assume_init_mut());
65-
}
66-
67-
let mut written = 0;
68-
loop {
69-
let len = match reader.read(unsafe { buf.assume_init_mut() }) {
70-
Ok(0) => return Ok(written),
71-
Ok(len) => len,
72-
Err(ref e) if e.kind() == ErrorKind::Interrupted => continue,
73-
Err(e) => return Err(e),
74-
};
75-
writer.write_all(unsafe { &buf.assume_init_ref()[..len] })?;
76-
written += len as u64;
77-
}
78-
}
7+
use crate::io::{self, BufRead, Initializer, IoSlice, IoSliceMut, Read, Write};
798

809
/// A reader which is always at EOF.
8110
///

library/std/src/lib.rs

+1
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,7 @@
317317
#![feature(toowned_clone_into)]
318318
#![feature(total_cmp)]
319319
#![feature(trace_macros)]
320+
#![feature(try_blocks)]
320321
#![feature(try_reserve)]
321322
#![feature(unboxed_closures)]
322323
#![feature(unsafe_block_in_unsafe_fn)]

library/std/src/sys/unix/fs.rs

+8-77
Original file line numberDiff line numberDiff line change
@@ -1204,88 +1204,19 @@ pub fn copy(from: &Path, to: &Path) -> io::Result<u64> {
12041204

12051205
#[cfg(any(target_os = "linux", target_os = "android"))]
12061206
pub fn copy(from: &Path, to: &Path) -> io::Result<u64> {
1207-
use crate::cmp;
1208-
use crate::sync::atomic::{AtomicBool, Ordering};
1209-
1210-
// Kernel prior to 4.5 don't have copy_file_range
1211-
// We store the availability in a global to avoid unnecessary syscalls
1212-
static HAS_COPY_FILE_RANGE: AtomicBool = AtomicBool::new(true);
1213-
1214-
unsafe fn copy_file_range(
1215-
fd_in: libc::c_int,
1216-
off_in: *mut libc::loff_t,
1217-
fd_out: libc::c_int,
1218-
off_out: *mut libc::loff_t,
1219-
len: libc::size_t,
1220-
flags: libc::c_uint,
1221-
) -> libc::c_long {
1222-
libc::syscall(libc::SYS_copy_file_range, fd_in, off_in, fd_out, off_out, len, flags)
1223-
}
1224-
12251207
let (mut reader, reader_metadata) = open_from(from)?;
12261208
let max_len = u64::MAX;
12271209
let (mut writer, _) = open_to_and_set_permissions(to, reader_metadata)?;
12281210

1229-
let has_copy_file_range = HAS_COPY_FILE_RANGE.load(Ordering::Relaxed);
1230-
let mut written = 0u64;
1231-
while written < max_len {
1232-
let copy_result = if has_copy_file_range {
1233-
let bytes_to_copy = cmp::min(max_len - written, usize::MAX as u64) as usize;
1234-
let copy_result = unsafe {
1235-
// We actually don't have to adjust the offsets,
1236-
// because copy_file_range adjusts the file offset automatically
1237-
cvt(copy_file_range(
1238-
reader.as_raw_fd(),
1239-
ptr::null_mut(),
1240-
writer.as_raw_fd(),
1241-
ptr::null_mut(),
1242-
bytes_to_copy,
1243-
0,
1244-
))
1245-
};
1246-
if let Err(ref copy_err) = copy_result {
1247-
match copy_err.raw_os_error() {
1248-
Some(libc::ENOSYS | libc::EPERM | libc::EOPNOTSUPP) => {
1249-
HAS_COPY_FILE_RANGE.store(false, Ordering::Relaxed);
1250-
}
1251-
_ => {}
1252-
}
1253-
}
1254-
copy_result
1255-
} else {
1256-
Err(io::Error::from_raw_os_error(libc::ENOSYS))
1257-
};
1258-
match copy_result {
1259-
Ok(0) if written == 0 => {
1260-
// fallback to work around several kernel bugs where copy_file_range will fail to
1261-
// copy any bytes and return 0 instead of an error if
1262-
// - reading virtual files from the proc filesystem which appear to have 0 size
1263-
// but are not empty. noted in coreutils to affect kernels at least up to 5.6.19.
1264-
// - copying from an overlay filesystem in docker. reported to occur on fedora 32.
1265-
return io::copy(&mut reader, &mut writer);
1266-
}
1267-
Ok(0) => return Ok(written), // reached EOF
1268-
Ok(ret) => written += ret as u64,
1269-
Err(err) => {
1270-
match err.raw_os_error() {
1271-
Some(
1272-
libc::ENOSYS | libc::EXDEV | libc::EINVAL | libc::EPERM | libc::EOPNOTSUPP,
1273-
) => {
1274-
// Try fallback io::copy if either:
1275-
// - Kernel version is < 4.5 (ENOSYS)
1276-
// - Files are mounted on different fs (EXDEV)
1277-
// - copy_file_range is broken in various ways on RHEL/CentOS 7 (EOPNOTSUPP)
1278-
// - copy_file_range is disallowed, for example by seccomp (EPERM)
1279-
// - copy_file_range cannot be used with pipes or device nodes (EINVAL)
1280-
assert_eq!(written, 0);
1281-
return io::copy(&mut reader, &mut writer);
1282-
}
1283-
_ => return Err(err),
1284-
}
1285-
}
1286-
}
1211+
use super::kernel_copy::{copy_regular_files, CopyResult};
1212+
1213+
match copy_regular_files(reader.as_raw_fd(), writer.as_raw_fd(), max_len) {
1214+
CopyResult::Ended(result) => result,
1215+
CopyResult::Fallback(written) => match io::copy::generic_copy(&mut reader, &mut writer) {
1216+
Ok(bytes) => Ok(bytes + written),
1217+
Err(e) => Err(e),
1218+
},
12871219
}
1288-
Ok(written)
12891220
}
12901221

12911222
#[cfg(any(target_os = "macos", target_os = "ios"))]

0 commit comments

Comments
 (0)