文件复制:File.Copy vs. 手动使用FileStream.Write进行复制

36

我的问题涉及文件复制性能。我们有一个媒体管理系统,需要在文件系统上将文件移动到不同的位置,包括同一网络上的Windows共享、FTP站点、AmazonS3等。当我们全部使用一个Windows网络时,我们可以使用 System.IO.File.Copy(source, destination) 来复制文件。由于很多时候我们只有输入流(如MemoryStream),我们尝试抽象Copy操作以接受输入流和输出流,但我们发现性能大幅下降。以下是一些用于复制文件的代码,供讨论。

public void Copy(System.IO.Stream inStream, string outputFilePath)
{
    int bufferSize = 1024 * 64;

    using (FileStream fileStream = new FileStream(outputFilePath, FileMode.OpenOrCreate, FileAccess.Write))
    {

        int bytesRead = -1;
        byte[] bytes = new byte[bufferSize];

        while ((bytesRead = inStream.Read(bytes, 0, bufferSize)) > 0)
        {
            fileStream.Write(bytes, 0, bytesRead);
            fileStream.Flush();
        }
    }
}

有人知道为什么这个操作比File.Copy()慢这么多吗?有没有任何方法可以提高它的性能?我是不是必须要特别设计逻辑来判断文件是否在同一个Windows位置中拷贝,如果是的话就用File.Copy(),否则就使用流?

请告诉我你的想法以及是否需要额外的信息。我已经尝试使用不同的缓冲区大小,似乎64k缓冲区大小对于我们的“小”文件是最优的,而256k+缓冲区大小对于我们的“大”文件更好一些,但无论如何,性能都远不如File.Copy()。预先感谢您!


3
这可能与本机互操作有关。我怀疑File.Copy()和流IO操作是基于Windows API构建的,并且在循环中重复调用流读/写比File.Copy()将执行的单个本机复制文件调用更昂贵。 - Steve Guidi
@Steve:你说得对,看我的回复。 - Ed S.
8个回答

26

File.Copy是基于CopyFile Win32函数构建的,这个函数受到了MS团队的大量关注(还记得关于缓慢复制性能的Vista相关线程吗)。

提高方法性能的几个提示:

  1. 像许多人之前所说,从您的循环中删除Flush方法。您根本不需要它。
  2. 增加缓冲区可能有所帮助,但仅适用于文件对文件操作,对于网络共享或ftp服务器,这将导致速度变慢。60 * 1024对于网络共享来说是理想的,至少在vista之前。对于ftp,在大多数情况下32k就足够了。
  3. 通过提供您的缓存策略(在您的情况下是顺序读取和写入),使用带有FileOptions参数(SequentalScan)的FileStream构造函数重载来帮助操作系统。
  4. 您可以使用异步模式加快复制速度(特别适用于网络到文件的情况),但不要使用线程,而是使用重叠io(.net中的BeginRead,EndRead,BeginWrite,EndWrite),并不要忘记在FileStream构造函数中设置Asynchronous选项(请参阅FileOptions

异步复制模式示例:

int Readed = 0;
IAsyncResult ReadResult;
IAsyncResult WriteResult;

ReadResult = sourceStream.BeginRead(ActiveBuffer, 0, ActiveBuffer.Length, null, null);
do
{
    Readed = sourceStream.EndRead(ReadResult);

    WriteResult = destStream.BeginWrite(ActiveBuffer, 0, Readed, null, null);
    WriteBuffer = ActiveBuffer;

    if (Readed > 0)
    {
      ReadResult = sourceStream.BeginRead(BackBuffer, 0, BackBuffer.Length, null, null);
      BackBuffer = Interlocked.Exchange(ref ActiveBuffer, BackBuffer);
    }

    destStream.EndWrite(WriteResult);
  }
  while (Readed > 0);

7

三个改变将显著提高性能:

  1. 增加缓冲区的大小,尝试1MB(嗯 - 只是试试)
  2. 打开文件流后,调用fileStream.SetLength(inStream.Length)来预先分配磁盘上的整个块(仅当inStream可寻址时才起作用)
  3. 删除fileStream.Flush() - 它是多余的,并且可能对性能产生最大的影响,因为它将阻塞直到刷新完成。在处理结束时,流将被自动刷新。

在我进行的实验中,这种方法的速度似乎快了3-4倍:

   public static void Copy(System.IO.Stream inStream, string outputFilePath)
    {
        int bufferSize = 1024 * 1024;

        using (FileStream fileStream = new FileStream(outputFilePath, FileMode.OpenOrCreate, FileAccess.Write))
        {
            fileStream.SetLength(inStream.Length);
            int bytesRead = -1;
            byte[] bytes = new byte[bufferSize];

            while ((bytesRead = inStream.Read(bytes, 0, bufferSize)) > 0)
            {
                fileStream.Write(bytes, 0, bytesRead);
            }
       }
    }

7
清理掉反射器,我们可以看到File.Copy实际上调用了Win32 API:
if (!Win32Native.CopyFile(fullPathInternal, dst, !overwrite))

这将解析为

[DllImport("kernel32.dll", CharSet=CharSet.Auto, SetLastError=true)]
internal static extern bool CopyFile(string src, string dst, bool failIfExists);

这里是CopyFile的文档


6

即使你用汇编仔细地编写代码,也无法在执行基本操作时击败操作系统。

如果您需要确保操作具有最佳性能,并且想要混合和匹配各种来源,则需要创建描述资源位置的类型。然后,您可以创建一个API,其中包含函数(例如Copy),该函数接受两个此类类型并检查两者的描述后选择性能最佳的复制机制。例如,如果确定了两个位置都是Windows文件位置,则会选择File.Copy;或者,如果源是Windows文件但目标是HTTP POST,则使用WebRequest。


1
尝试移除Flush调用,并将其移到循环外部。
有时候操作系统最清楚何时刷新IO。这样可以更好地利用其内部缓冲区。

我也不认为复制操作涉及多线程,个人认为这是一个不好的想法。这意味着为每个复制操作创建一个线程,这据说比仅使用流更加昂贵。 - Aviad Ben Dov
@aviadbenov:确实,创建自己的线程来处理IO操作是杀鸡焉用牛刀。然而,.NET会专门为此保持一组线程池。正确使用异步IO调用能够让我们利用这些线程,而无需自己创建和销毁它们。 - AnthonyWJones
@Anthony:你说的没错,但也很危险。如果有许多线程在复制文件,线程池本身就会成为复制操作的瓶颈! - Aviad Ben Dov

1

1

Mark Russinovich 是这方面的权威。

他在他的博客上写了一篇《Vista SP1文件复制改进内幕》,总结了Windows通过Vista SP1的最新技术。

我的初步猜测是,在大多数情况下,File.Copy会是最强大的。当然,这并不意味着在某些特定的角落情况下,你自己的代码可能会胜过它...


0

有一件事很明显,那就是你正在读取一块数据,写入该块数据,再读取另一块数据,如此反复。

流式操作非常适合多线程处理。我猜测 File.Copy 实现了多线程。

尝试在一个线程中读取,在另一个线程中写入。你需要协调好线程,使得写入线程不会在读取线程填满缓存之前开始写入。可以通过使用两个缓存来解决这个问题,其中一个正在被读取,而另一个正在被写入,并且有一个标志来说明当前哪个缓存正在被用于哪个目的。


我目前正在研究多线程。也许有一些很好的开源项目可以做到这一点?我会继续调查。感谢您的快速回复。 - jakejgordon

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接