Windows x64上Python x64复制文件性能评估/问题

5 投票
4 回答
1572 浏览
提问于 2025-04-16 07:36

在开发一种备份应用程序时,我对Windows上的文件复制性能进行了评估。

我有几个问题,想听听你们的看法。

谢谢!

卢卡斯。

问题:

  1. 为什么复制10 GiB的文件时性能比复制1 GiB的文件慢这么多?

  2. 为什么shutil.copyfile这么慢?

  3. 为什么win32file.CopyFileEx这么慢?这会不会是因为使用了win32file.COPY_FILE_RESTARTABLE这个标志?不过,它不接受整数1000作为标志(COPY_FILE_NO_BUFFERING),而这个标志是推荐用于大文件的:http://msdn.microsoft.com/en-us/library/aa363852%28VS.85%29.aspx

  4. 使用一个空的ProgressRoutine似乎对没有使用ProgressRoutine的效果没有影响。

  5. 有没有其他更高效的文件复制方法,同时还能获取进度更新?

1 GiB和10 GiB文件的结果:

test_file_size             1082.1 MiB    10216.7 MiB

METHOD                      SPEED           SPEED
robocopy.exe                111.0 MiB/s     75.4 MiB/s
cmd.exe /c copy              95.5 MiB/s     60.5 MiB/s
shutil.copyfile              51.0 MiB/s     29.4 MiB/s
win32api.CopyFile           104.8 MiB/s     74.2 MiB/s
win32file.CopyFile          108.2 MiB/s     73.4 MiB/s
win32file.CopyFileEx A       14.0 MiB/s     13.8 MiB/s
win32file.CopyFileEx B       14.6 MiB/s     14.9 MiB/s

测试环境:

Python:
ActivePython 2.7.0.2 (ActiveState Software Inc.) based on
Python 2.7 (r27:82500, Aug 23 2010, 17:17:51) [MSC v.1500 64 bit (AMD64)] on win32

source = mounted network drive
source_os = Windows Server 2008 x64

destination = local drive
destination_os = Windows Server 2008 R2 x64

备注:

'robocopy.exe' and 'cmd.exe /c copy' were run using subprocess.call()

win32file.CopyFileEx A(不使用ProgressRoutine):

def Win32_CopyFileEx_NoProgress( ExistingFileName, NewFileName):
    win32file.CopyFileEx(
        ExistingFileName,                             # PyUNICODE           | File to be copied
        NewFileName,                                  # PyUNICODE           | Place to which it will be copied
        None,                                         # CopyProgressRoutine | A python function that receives progress updates, can be None
        Data = None,                                  # object              | An arbitrary object to be passed to the callback function
        Cancel = False,                               # boolean             | Pass True to cancel a restartable copy that was previously interrupted
        CopyFlags = win32file.COPY_FILE_RESTARTABLE,  # int                 | Combination of COPY_FILE_* flags
        Transaction = None                            # PyHANDLE            | Handle to a transaction as returned by win32transaction::CreateTransaction
        )

win32file.CopyFileEx B(使用空的ProgressRoutine):

def Win32_CopyFileEx( ExistingFileName, NewFileName):
    win32file.CopyFileEx(
        ExistingFileName,                             # PyUNICODE           | File to be copied
        NewFileName,                                  # PyUNICODE           | Place to which it will be copied
        Win32_CopyFileEx_ProgressRoutine,             # CopyProgressRoutine | A python function that receives progress updates, can be None
        Data = None,                                  # object              | An arbitrary object to be passed to the callback function
        Cancel = False,                               # boolean             | Pass True to cancel a restartable copy that was previously interrupted
        CopyFlags = win32file.COPY_FILE_RESTARTABLE,  # int                 | Combination of COPY_FILE_* flags
        Transaction = None                            # PyHANDLE            | Handle to a transaction as returned by win32transaction::CreateTransaction
        )

def Win32_CopyFileEx_ProgressRoutine(
    TotalFileSize,
    TotalBytesTransferred,
    StreamSize,
    StreamBytesTransferred,
    StreamNumber,
    CallbackReason,                         # CALLBACK_CHUNK_FINISHED or CALLBACK_STREAM_SWITCH
    SourceFile,
    DestinationFile,
    Data):                                  # Description
    return win32file.PROGRESS_CONTINUE      # return of any win32file.PROGRESS_* constant

4 个回答

2

关于你的第二个问题:

shutil.copyfile() 之所以慢,是因为它默认使用的是16KB的复制缓冲区。最后它会调用 shutil.copyfileobj(),这个函数大概是这样的:

def copyfileobj(fsrc, fdst, length=16*1024):
    """copy data from file-like object fsrc to file-like object fdst"""
    while 1:
        buf = fsrc.read(length)
        if not buf:
            break
        fdst.write(buf)

在你的情况下,它在读取16KB和写入16KB之间来回切换。如果你直接在你的GB文件上使用 copyfileobj(),而且把缓冲区设置成128MB,比如这样,你会发现性能会大幅提升。

3

很可能是因为你测量完成时间的方式不同。

我猜1GB的文件在内存里是可以轻松放下的。所以操作系统可能只是把它缓存起来,然后告诉你的应用程序说已经复制完成了,但实际上大部分(可能是全部)数据还在内核的缓冲区里没有写入磁盘。

而10GB的文件就放不下了,所以在说完成之前,它必须先把大部分数据写入磁盘。

如果你想要一个有意义的测量结果,

a) 在每次运行之前清空文件系统的缓存 - 如果你的操作系统没有方便的方法来做到这一点,可以重启一下(注意:Windows好像没有方便的方法,不过我记得有个系统内部工具可以做到)。如果是网络文件系统,也要在服务器上清空缓存。

b) 在测量完成时间之前,确保文件已经同步到磁盘。

这样的话,我想你会看到更一致的时间结果。

3

问题 3:

你对微软API中的COPY_FILE_NO_BUFFERING标志理解错了。它不是整数1000,而是十六进制的1000(0x1000 => 整数值:4096)。当你把CopyFlags设置为4096时,你就会在Windows环境中拥有最快的复制方式。我在我的数据备份代码中使用了同样的方式,它非常快,每天都能传输TB级别的数据。

问题 4:

这没关系,因为它是一个回调函数。但总体来说,你不应该在里面放太多代码,要保持代码简洁明了。

问题 5:

根据我的经验,这是在标准Windows环境中最快的复制方式。可能会有更快的自定义复制方式,但如果使用普通的Windows API,就找不到比这个更好的了。

撰写回答