Use copy_file_range(2), if available, to implement File.Copy#39235
Use copy_file_range(2), if available, to implement File.Copy#39235lpereira wants to merge 1 commit intodotnet:masterfrom
Conversation
|
Build breaks... |
2470f49 to
4e32dc1
Compare
Should be fixed. That reminds me that we should evaluating building with |
|
Post 3.0 at this point I think. |
|
Marking no merge until master becomes 5.0. |
|
We build our official binaries on CentOS 7, which AFAIK has glibc 2.17. That means that they will not be able to take advantage of this change. So it seems it would be better to use the syscall directly. |
Considering that we have a |
4e32dc1 to
02c5787
Compare
|
Updated patch to use |
f7e8020 to
b5c41c8
Compare
|
@lpereira would you be able to take a look? Thanks |
|
@filipnavara is fine in rebasing #37583 on top of this one; could I please get some review here? (I leave on vacations this week, would like to get this merged before then. Build breaks seem spurious.) |
|
Could you please verify that the optimization will kick in for official builds that are built on CentOS 7? |
6eb6a73 to
4db6819
Compare
I force-pushed a new version that hardcodes the syscall number on x86-64, i386, arm32, and arm64 (falling back to including syscall.h and trying to use __NR_copy_file_range). It should be enabled when built with CentOS 7. |
|
The Linux test failures seems to be caused by this change. |
4db6819 to
faa0054
Compare
|
Rebased and repushed with some of the comments addressed. |
From the man page:
The copy_file_range() system call performs an in-kernel
copy between two file descriptors without the additional cost
of transferring data from the kernel to user space and then
back into the kernel.
This system call, available in some modern-ish Linux systems, will try
to copy between file descriptors using the most efficient way according
to the filesystem. If none of the filesystems involved are able to
perform a efficient copy, it'll fallback to using splice(2), which is
essentially how sendfile(2) -- the current method to perform a file
copy -- is implemented.
Although the GNU C Library provides a syscall wrapper since 2.27, our
build servers are based off of an older version of CentOS, which do not
ship with that library. So the system call is called directly with
syscall() instead. Since glibc 2.29, there's a user mode fallback too,
but we don't need it as there are two other fallback mechanisms in place
already.
faa0054 to
aad35fb
Compare
|
@JeremyKuhne @carlossanlop could you please re-review this oldie? |
|
it also blocks #37583 |
| } | ||
|
|
||
| toCopy -= (size_t)copied; | ||
| if (toCopy == 0) |
There was a problem hiding this comment.
Is there a chance that toCopy could end up being less than 0? Should this check be if (toCopy <= 0) or is there no point?
There was a problem hiding this comment.
There's no point: copied will either be -1 (and that's handled above), or, at most, the value of toCopy (it'll return the number of bytes actually copied, which could be less than that). If for some reason the syscall returns more than the value of toCopy (which is passed as a parameter), then there's a bug in the kernel.
(Also, toCopy is size_t, which is unsigned.)
carlossanlop
left a comment
There was a problem hiding this comment.
I have a question. If it's not actionable, let me know, so I can approve, @lpereira.
| return fcopyfile(inFd, outFd, NULL, COPYFILE_ALL) == 0 ? 0 : -1; | ||
| #else | ||
| // Get the stats on the source file. | ||
| // Now that the data from the file has been copied, copy over metadata |
There was a problem hiding this comment.
This comment no longer makes sense now that it's been separated into its own function. Let's delete the "Now that the data from the file has been copied, " part.
| { | ||
| // If copy_file_range is unavailable in this kernel, size | ||
| // will remain untouched, so it's fine for concurrent | ||
| // threads to write to sendFileUnavailable. |
There was a problem hiding this comment.
I don't understand this comment... what does this check have to do with sendFileUnavailable? Is that a copy-and-paste error in the comment?
| if (toCopy == 0) | ||
| { | ||
| break; | ||
| } |
There was a problem hiding this comment.
Why not just make the condition on the loop while (toCopy > 0)? That would also avoid entering the loop if the amount of data to copy initially is 0.
| if (ret != 0) | ||
|
|
||
| startOffset += (off_t)(sourceStat.st_size - (off_t)bytesToCopy); | ||
| // copy_file_range couldn't be used, either because we're in an older kernel or |
There was a problem hiding this comment.
| // copy_file_range couldn't be used, either because we're in an older kernel or | |
| // copy_file_range couldn't be used to copy the whole file, either because we're in an older kernel or |
| // Then copy permissions. | ||
| while ((ret = fchmod(outFd, sourceStat.st_mode & (S_IRWXU | S_IRWXG | S_IRWXO))) < 0 && errno == EINTR); | ||
| if (ret != 0) | ||
| // sendfile couldn't be used; fall back to a manual copy below. This could happen |
There was a problem hiding this comment.
| // sendfile couldn't be used; fall back to a manual copy below. This could happen | |
| // sendfile couldn't be used to copy the whole file; fall back to a manual copy below. This could happen |
| #endif // !HAVE_FCOPYFILE | ||
|
|
||
| int32_t SystemNative_CopyFile(intptr_t sourceFd, intptr_t destinationFd) | ||
| static int32_t CopyFileMetadata(int inFd, int outFd, struct stat_ *sourceStat) |
There was a problem hiding this comment.
Why does CopyFileMetadata need to take a pointer to a struct stat_ rather than just declaring one itself? None of the call sites use the passed in pointer after the call, and I don't think the benefit of avoiding the extra local outweighs the cognitive cost of passing around pointers unnecessarily.
| #endif // HAVE_SENDFILE_4 | ||
|
|
||
| #if defined(PAL_COPY_FILE_RANGE_SYSCALL) | ||
| static loff_t CopyFileRange(int inFd, loff_t *inOff, int outFd, loff_t *outOff, size_t len, unsigned int flags) |
There was a problem hiding this comment.
Would it make sense to name this copy_file_range and define it only if it's not already defined?
stephentoub
left a comment
There was a problem hiding this comment.
Some comments to be addressed, otherwise LGTM.
Can you share some perf numbers using File.Copy to highlight the benefits of the change?
|
In addition to all of the comments to be addressed, this now also has conflicts. @lpereira, do you plan to finalize this PR in the next week and a half? If not, we should close it and you can submit an updated version in the new dotnet/runtime repo when it's available. |
|
@stephentoub In order to make things easier for the repo consolidation, I'll do this and come back with an updated patch once that happens. |
|
Ok. Sounds good, thanks. |
From the man page:
This system call, available in some modern-ish Linux systems, will try
to copy between file descriptors using the most efficient way according
to the filesystem. If none of the filesystems involved are able to
perform a efficient copy, it'll fallback to using splice(2), which is
essentially how sendfile(2) is implemented.
The GNU C Library provides a syscall wrapper since 2.27. Since 2.29,
it also provides a userspace fallback mechanism (similar to
CopyFile_ReadWrite(), using read(2) and write(2)). The reason for not
using syscall(2) directly instead of relying on the syscall wrapper is
precisely to use the glibc userspace copy fallback instead.
Should fix #39231.