-
-
Notifications
You must be signed in to change notification settings - Fork 30.5k
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
bpo-33671: efficient zero-copy for shutil.copy* functions (Linux, OSX and Win) #7160
Changes from 70 commits
1a72c01
77c4bfa
2afa04a
542cd17
3520c6c
050a722
c1fd38a
2ab6317
dacc3b6
29d5881
114c4de
501c0dd
41b4506
fdb0973
64d2bc5
3a3c8ef
7861737
f3eecfd
f67ce57
d457254
8eb211d
a0fe703
7296147
d0c3bba
2cafd80
bb2a75f
e5025dc
a36a534
e9da3fa
9fcc2e7
4f32242
24ad25a
24d20e6
7b6e576
b82ddc9
b62b61e
34e9618
6b20902
abf3ecb
91e492c
e02c69d
73837e2
28be4c1
6c59adf
700629d
077912e
62c6568
a40a755
7ba0085
6c96d97
80fbe6e
fdf4bcb
185f130
c8c98ae
17bb5e6
d8b9bf9
b59ac57
8eefce7
4fc8c6b
3048e3d
11102e1
7545273
3261b74
51c476d
729dd23
1823828
a9d6a07
e3ce917
f81a0ec
3e7475b
05dd3cf
9b54930
2bec11c
c87648f
941f740
4d28c12
2149b8b
6a02a2a
2287508
b9da5d5
c921f46
bb24490
fef8b32
71be453
6035fe2
8dc651e
5d0eada
d67cdc5
f65c8ae
9c4508e
bb1fee6
566898a
f435053
30c9a57
33f362f
e17e729
bc46f75
cabbc02
d22ee08
7a08203
ab284e9
ac9479d
fd77a7e
42a597e
5008a8d
e89dd20
c0dc4b8
29b9730
a1bed32
d9d27a7
17bd78b
b1d4917
5ce94e4
07bcef5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -51,7 +51,9 @@ Directory and files operations | |
.. function:: copyfile(src, dst, *, follow_symlinks=True) | ||
|
||
Copy the contents (no metadata) of the file named *src* to a file named | ||
*dst* and return *dst*. *src* and *dst* are path names given as strings. | ||
*dst* and return *dst* in the most efficient way possible. | ||
*src* and *dst* are path names given as strings. | ||
|
||
*dst* must be the complete target file name; look at :func:`shutil.copy` | ||
for a copy that accepts a target directory path. If *src* and *dst* | ||
specify the same file, :exc:`SameFileError` is raised. | ||
|
@@ -74,6 +76,10 @@ Directory and files operations | |
Raise :exc:`SameFileError` instead of :exc:`Error`. Since the former is | ||
a subclass of the latter, this change is backward compatible. | ||
|
||
.. versionchanged:: 3.8 | ||
Platform-specific fast-copy syscalls are used internally in order to copy | ||
the file more efficiently. See | ||
:ref:`shutil-platform-dependent-efficient-copy-operations` section. | ||
|
||
.. exception:: SameFileError | ||
|
||
|
@@ -163,6 +169,11 @@ Directory and files operations | |
Added *follow_symlinks* argument. | ||
Now returns path to the newly created file. | ||
|
||
.. versionchanged:: 3.8 | ||
Platform-specific fast-copy syscalls are used internally in order to copy | ||
the file more efficiently. See | ||
:ref:`shutil-platform-dependent-efficient-copy-operations` section. | ||
|
||
.. function:: copy2(src, dst, *, follow_symlinks=True) | ||
|
||
Identical to :func:`~shutil.copy` except that :func:`copy2` | ||
|
@@ -185,6 +196,11 @@ Directory and files operations | |
file system attributes too (currently Linux only). | ||
Now returns path to the newly created file. | ||
|
||
.. versionchanged:: 3.8 | ||
Platform-specific fast-copy syscalls are used internally in order to copy | ||
the file more efficiently. See | ||
:ref:`shutil-platform-dependent-efficient-copy-operations` section. | ||
|
||
.. function:: ignore_patterns(\*patterns) | ||
|
||
This factory function creates a function that can be used as a callable for | ||
|
@@ -241,6 +257,10 @@ Directory and files operations | |
Added the *ignore_dangling_symlinks* argument to silent dangling symlinks | ||
errors when *symlinks* is false. | ||
|
||
.. versionchanged:: 3.8 | ||
Platform-specific fast-copy syscalls are used internally in order to copy | ||
the file more efficiently. See | ||
:ref:`shutil-platform-dependent-efficient-copy-operations` section. | ||
|
||
.. function:: rmtree(path, ignore_errors=False, onerror=None) | ||
|
||
|
@@ -314,6 +334,11 @@ Directory and files operations | |
.. versionchanged:: 3.5 | ||
Added the *copy_function* keyword argument. | ||
|
||
.. versionchanged:: 3.8 | ||
Platform-specific fast-copy syscalls are used internally in order to copy | ||
the file more efficiently. See | ||
:ref:`shutil-platform-dependent-efficient-copy-operations` section. | ||
|
||
.. function:: disk_usage(path) | ||
|
||
Return disk usage statistics about the given path as a :term:`named tuple` | ||
|
@@ -370,6 +395,29 @@ Directory and files operations | |
operation. For :func:`copytree`, the exception argument is a list of 3-tuples | ||
(*srcname*, *dstname*, *exception*). | ||
|
||
.. _shutil-platform-dependent-efficient-copy-operations: | ||
|
||
Platform-dependent efficient copy operations | ||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | ||
|
||
Starting from Python 3.8 all functions involving a file copy (:func:`copyfile`, | ||
:func:`copy`, :func:`copy2`, :func:`copytree`, and :func:`move`) use | ||
platform-specific "fast-copy" syscalls in order to copy the file more | ||
efficiently (see :issue:`33671`). | ||
"fast-copy" means that the copying operation occurs within the kernel, avoiding | ||
the use of userspace buffers in Python as in "``outfd.write(infd.read())``". | ||
|
||
On OSX `fcopyfile`_ is used to copy the file content (not metadata). | ||
On Linux, Solaris and other POSIX platforms | ||
where :func:`os.sendfile` supports copies between 2 regular file descriptors | ||
:func:`os.sendfile` is used. | ||
On Windows `CopyFile`_ is used by all copy functions except :func:`copyfile`. | ||
|
||
If the fast-copy operation fails and no data was written in the destination | ||
file then shutil will silently fallback on using less efficient | ||
:func:`copyfileobj` function internally. | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Given @eryksun's comments about how the relevant Windows API works, it may be better to just name the platform specific APIs here, without going into details on exactly how those APIs achieve their performance gains relative to a Python level copy operation. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Agreed. Done. |
||
.. versionadded:: 3.8 | ||
|
||
.. _shutil-copytree-example: | ||
|
||
|
@@ -654,6 +702,11 @@ Querying the size of the output terminal | |
|
||
.. versionadded:: 3.3 | ||
|
||
.. _`CopyFile`: | ||
https://msdn.microsoft.com/en-us/library/windows/desktop/aa363851(v=vs.85).aspx | ||
|
||
.. _`fcopyfile`: | ||
http://www.manpagez.com/man/3/fcopyfile/ | ||
|
||
.. _`Other Environment Variables`: | ||
http://pubs.opengroup.org/onlinepubs/7908799/xbd/envvar.html#tag_002_003 | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -42,6 +42,16 @@ | |
except ImportError: | ||
getgrnam = None | ||
|
||
posix = nt = None | ||
if os.name == 'posix': | ||
import posix | ||
elif os.name == 'nt': | ||
import nt | ||
import _winapi | ||
|
||
_HAS_SENDFILE = posix and hasattr(os, "sendfile") | ||
_HAS_FCOPYFILE = posix and hasattr(posix, "_fcopyfile") | ||
|
||
__all__ = ["copyfileobj", "copyfile", "copymode", "copystat", "copy", "copy2", | ||
"copytree", "move", "rmtree", "Error", "SpecialFileError", | ||
"ExecError", "make_archive", "get_archive_formats", | ||
|
@@ -72,6 +82,10 @@ class RegistryError(Exception): | |
"""Raised when a registry operation with the archiving | ||
and unpacking registries fails""" | ||
|
||
class _GiveupOnFastCopy(Exception): | ||
"""Raised as a signal to fallback on using raw read()/write() | ||
file copy when fast-copy functions fail to do so. | ||
""" | ||
|
||
def copyfileobj(fsrc, fdst, length=16*1024): | ||
"""copy data from file-like object fsrc to file-like object fdst""" | ||
|
@@ -81,6 +95,104 @@ def copyfileobj(fsrc, fdst, length=16*1024): | |
break | ||
fdst.write(buf) | ||
|
||
def _fastcopy_osx(fsrc, fdst): | ||
"""Copy 2 regular mmap-like files by using high-performance | ||
fcopyfile() syscall (OSX only). | ||
""" | ||
try: | ||
infd = fsrc.fileno() | ||
outfd = fdst.fileno() | ||
except Exception as err: | ||
raise _GiveupOnFastCopy(err) # not a regular file | ||
|
||
try: | ||
posix._fcopyfile(infd, outfd) | ||
except OSError as err: | ||
if err.errno in {errno.EINVAL, errno.ENOTSUP}: | ||
raise _GiveupOnFastCopy(err) | ||
else: | ||
raise err from None | ||
|
||
def _fastcopy_win(fsrc, fdst): | ||
"""Copy 2 files by using high-performance CopyFileW (Windows only).""" | ||
_winapi.CopyFileExW(fsrc, fdst, 0) | ||
|
||
def _fastcopy_sendfile(fsrc, fdst): | ||
"""Copy data from one regular mmap-like fd to another by using | ||
high-performance sendfile() method. | ||
This should work on Linux >= 2.6.33 and Solaris only. | ||
""" | ||
global _HAS_SENDFILE | ||
try: | ||
infd = fsrc.fileno() | ||
outfd = fdst.fileno() | ||
except Exception as err: | ||
raise _GiveupOnFastCopy(err) # not a regular file | ||
|
||
# Hopefully the whole file will be copied in a single call. | ||
# sendfile() is called in a loop 'till EOF is reached (0 return) | ||
# so a bufsize smaller or bigger than the actual file size | ||
# should not make any difference, also in case the file content | ||
# changes while being copied. | ||
try: | ||
blocksize = max(os.fstat(infd).st_size, 2 ** 23) # min 8MB | ||
except Exception: | ||
blocksize = 2 ** 27 # 128MB | ||
|
||
offset = 0 | ||
while True: | ||
try: | ||
sent = os.sendfile(outfd, infd, offset, blocksize) | ||
except OSError as err: | ||
if err.errno == errno.ENOTSOCK: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This path will also be hit consistently on FreeBSD and other systems that offer To avoid a performance regression on such platforms, it would be strongly preferred for there to be a build time configure check that told Rather than a new flag in the OS module, this support could be indicated to the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This would only be hit once though, as per There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How about in addition to setting the flag, we also do There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. To be honest I would prefer not to change module objects at runtime like this, even if the function is private. It makes testing and mocking more difficult (because tests rely on the private functions) and the speedup of avoiding a global |
||
# sendfile() on this platform (probably Linux < 2.6.33) | ||
# does not support copies between regular files (only | ||
# sockets). | ||
_HAS_SENDFILE = False | ||
raise _GiveupOnFastCopy(err) | ||
|
||
if err.errno == errno.ENOSPC: # filesystem is full | ||
raise err from None | ||
|
||
# Give up on first call and if no data was copied. | ||
if offset == 0 and os.lseek(outfd, 0, os.SEEK_CUR) == 0: | ||
raise _GiveupOnFastCopy(err) | ||
|
||
raise err from None | ||
else: | ||
if sent == 0: | ||
break # EOF | ||
offset += sent | ||
|
||
def _fastcopy_fileobj(fsrc, fdst): | ||
"""Copy 2 regular mmap-like fds by using zero-copy sendfile(2) | ||
(Linux) and fcopyfile(2) (OSX) syscalls. | ||
In case of error fallback on using plain read()/write() if no | ||
data was copied. | ||
""" | ||
# Note: copyfileobj() is left alone in order to not introduce any | ||
# unexpected breakage. Possible risks by using zero-copy calls | ||
# in copyfileobj() are: | ||
# - fdst cannot be open in "a"(ppend) mode | ||
# - fsrc and fdst may be open in "t"(ext) mode | ||
# - fsrc may be a BufferedReader (which hides unread data in a buffer), | ||
# GzipFile (which decompresses data), HTTPResponse (which decodes | ||
# chunks). | ||
# - possibly others | ||
if _HAS_SENDFILE: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Given a configure check for The dynamic check to handle the case where a Python built on a newer distro is run against an older kernel (and hence still gets There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. See my comment above. |
||
try: | ||
return _fastcopy_sendfile(fsrc, fdst) | ||
except _GiveupOnFastCopy: | ||
pass | ||
|
||
if _HAS_FCOPYFILE: | ||
try: | ||
return _fastcopy_osx(fsrc, fdst) | ||
except _GiveupOnFastCopy: | ||
pass | ||
|
||
return copyfileobj(fsrc, fdst) | ||
|
||
def _samefile(src, dst): | ||
# Macintosh, Unix. | ||
if hasattr(os.path, 'samefile'): | ||
|
@@ -117,9 +229,13 @@ def copyfile(src, dst, *, follow_symlinks=True): | |
if not follow_symlinks and os.path.islink(src): | ||
os.symlink(os.readlink(src), dst) | ||
else: | ||
if os.name == 'nt': | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This cannot be used in |
||
_fastcopy_win(src, dst) | ||
return dst | ||
|
||
with open(src, 'rb') as fsrc: | ||
with open(dst, 'wb') as fdst: | ||
copyfileobj(fsrc, fdst) | ||
_fastcopy_fileobj(fsrc, fdst) | ||
return dst | ||
|
||
def copymode(src, dst, *, follow_symlinks=True): | ||
|
@@ -1015,7 +1131,6 @@ def disk_usage(path): | |
|
||
elif os.name == 'nt': | ||
|
||
import nt | ||
__all__.append('disk_usage') | ||
_ntuple_diskusage = collections.namedtuple('usage', 'total used free') | ||
|
||
|
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.
Is there a difference between
fast-copy
andzero-copy
sys calls? #learningThere 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.
Initially I phrased this as "zero-copy" because that's what
os.sendfile()
actually does under the hoods. Then it turns out Windows does something different (see #7160 (comment)). As such it made more sense to stick with the generic "fast-copy" term. =)