-
Notifications
You must be signed in to change notification settings - Fork 12.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
fs: copy: use copy_file_range on Linux #50772
Conversation
Thanks for the pull request, and welcome! The Rust team is excited to review your changes, and you should hear from @TimNN (or someone else) soon. If any changes to this PR are deemed necessary, please add them as extra commits. This ensures that the reviewer can see what has changed since they last reviewed the code. Due to the way GitHub handles out-of-date commits, this should also make it reasonably obvious what issues have or haven't been addressed. Large or tricky changes may require several passes of review and changes. Please see the contribution instructions for more information. |
The job Click to expand the log.
I'm a bot! I can only do what humans tell me to, so if this was not helpful or you have suggestions for improvements, please ping or otherwise contact |
Thanks for the PR! As far as I can tell the implementation looks mostly good. While reviewing it would have been awesome if some more comments about the syscall had been present (mostly with regard to the syscall updating / reading the file offset, so no need to use Do we care about the (impossible?) edge case of some calls to Is it possible for this to (significantly) slow down the case where I think this needs some more carful handling of the cc @rust-lang/libs: What do you think about this kind of specialised code? |
We're generally amenable to using newer features when detected so long as it's functionally the same as what's there today. That's how things like getrandom, accept4, etc. work. Most of the time, however, we do the feature detection once and then cache what happened in a global, and that way all future calls don't continue to get ENOSYS or do feature detection |
Ping @TimNN @alexcrichton : IMO, it doesn't make sense to store the availability in a global here:
|
@nicokoch if that's the case, can you whip up a benchmark copying lots of small files and see what the perf impact of this change is for a system that doesn't support the syscall? |
If Implementing such a thing is definitely possible and feasible. Here's a short sketch (based on PRd code slightly): static HAS_COPY_FILE_RANGE: AtomicBool = AtomicBool::new(true);
let copy_result = if HAS_COPY_FILE_RANGE.load(Relaxed) {
copy_file_range(...)
} else {
ENOSYS
};
if copy_result == ENOSYS {
HAS_COPY_FILE_RANGE.store(false, Relaxed);
}
if copy_result == ENOSYS || copy_result == EXDEV {
// attempt simple copy
}
|
Thanks @nagisa . Will implement with the help of your snippet |
Ping |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If at all possible, could you extend the benchmark results in your repo with a comparison along the lines that @alexcrichton has pointed out?
I'd be interested in comparing the performance numbers of copying many (different) smaller files
- With
copy_file_range
on the same filesystem - With
copy_file_range
across filesystems (so the atomic check does not apply) - Without
copy_file_range
(so the atomic check applies)
src/libstd/sys/unix/fs.rs
Outdated
} else { | ||
(len - written) as usize | ||
}; | ||
let copy_result = if HAS_COPY_FILE_RANGE.load(Ordering::Relaxed) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would move this out of the loop (store the result of the load in a local variable). I don't know if there is any noteworthy overhead of reading relaxed atomic but that feels like a safer alternative.
src/libstd/sys/unix/fs.rs
Outdated
|
||
let mut written = 0u64; | ||
while written < len { | ||
let bytes_to_copy = if len - written > usize::max_value() as u64 { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ideally this would use TryFrom
, but that appears to be not implemented for u64 -> usize
case.
This implementation could fail for usize == u128
but I don't think this is something we are supporting right now.
- Move loading of atomic bool outside the loop - Add comment about TryFrom for future improvement
I implemented the atomic load before the loop and added a comment about TryFrom. I will see what I can do about the benchmarks :) |
The job Click to expand the log.
I'm a bot! I can only do what humans tell me to, so if this was not helpful or you have suggestions for improvements, please ping or otherwise contact |
@TimNN I have added the benchmark results and some notes in my repo's readme: |
Ping from triage @TimNN! The author should have addressed your comments. |
src/libstd/sys/unix/fs.rs
Outdated
let has_copy_file_range = HAS_COPY_FILE_RANGE.load(Ordering::Relaxed); | ||
let mut written = 0u64; | ||
while written < len { | ||
// TODO should ideally use TryFrom |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use FIXME instead (or the linter complains).
Thanks for the extensive benchmarks @nicokoch, awesome work! The benchmark results look fine to me, the atomic check introduces negligible overhead but there is some (small, and for larger files probably negligible) overhead when copying many small files across devices (because an additional syscall happens for every copy operation). I would propose to merge this, however @alexcrichton, can you also please take a look at the benchmark results in https://github.com/nicokoch/fastcopy? (And, should that be needed, propose an FCP or alternatively let us know what you think). |
Thanks! Note that benchmarks with fs stuff are kind of unreliable and vary depending on background Prozesses and things like i/o scheduling. Sometimes, my implementation was even faster in the EXDEV case, which is logically impossible. So take them with a grain of salt. |
Linter complaints are also fixed now |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changes look good to me other than a few nits, and the benchmarks also look good too! Thanks so much again @nicokoch!
src/libstd/sys/unix/fs.rs
Outdated
while written < len { | ||
let copy_result = if has_copy_file_range { | ||
// FIXME: should ideally use TryFrom | ||
let bytes_to_copy = if len - written > usize::max_value() as u64 { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps this?
let bytes_to_copy = cmp::min(len - written, usize::max_value() as u64) as usize;
match err.raw_os_error() { | ||
Some(os_err) if os_err == libc::ENOSYS || os_err == libc::EXDEV => { | ||
// Either kernel is too old or the files are not mounted on the same fs. | ||
// Try again with fallback method |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you also throw an assert in here that written
is 0? I don't think it's possible for it to not be 0 but the return value at least I believe will be invalid if it's nonzero
- compute bytes_to_copy more elegantly - add assert that written is 0 in fallback case
Thanks for the review @alexcrichton ! Nits were addressed in the latest commit. |
@bors: r+ |
📌 Commit c7d6a01 has been approved by |
fs: copy: use copy_file_range on Linux Linux 4.5 introduced a new system call [copy_file_range](http://man7.org/linux/man-pages/man2/copy_file_range.2.html) to copy data from one file to another. This PR uses the new system call (if available). This has several advantages: 1. No need to constantly copy data from userspace to kernel space, if the buffer is small or the file is large 2. On some filesystems, like BTRFS, the kernel can leverage internal fs mechanisms for huge performance gains 3. Filesystems on the network dont need to copy data between the host and the client machine (they have to in the current read/write implementation) I have created a small library that also implements the new system call for some huge performance gains here: https://github.com/nicokoch/fastcopy Benchmark results are in the README
☀️ Test successful - status-appveyor, status-travis |
}; | ||
if let Err(ref copy_err) = copy_result { | ||
if let Some(libc::ENOSYS) = copy_err.raw_os_error() { | ||
HAS_COPY_FILE_RANGE.store(false, Ordering::Relaxed); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't has_copy_file_range
be updated to false as well ?
For the first file, after the first chunk of the file has been copied, if copy_file_range was not available, next iterations of the while loop will try again to use copy_file_range and fail continuously, we could avoid it, making mut has_copy_file_range
and here has_copy_file_range = false;
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No. If copy_result is Err the loop will always terminate in the current iteration.
Linux 4.5 introduced a new system call copy_file_range to copy data from one file to another.
This PR uses the new system call (if available). This has several advantages:
I have created a small library that also implements the new system call for some huge performance gains here: https://github.com/nicokoch/fastcopy
Benchmark results are in the README