File.Copy 与手动 FileStream.Write 用于复制文件

Posted

技术标签:

【中文标题】File.Copy 与手动 FileStream.Write 用于复制文件【英文标题】:File.Copy vs. Manual FileStream.Write For Copying File 【发布时间】:2010-11-17 19:53:13 【问题描述】:

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

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( )。提前致谢!

【问题讨论】:

这可能与本机互操作有关。我怀疑 File.Copy() 和流 IO 操作是建立在 Windows API 之上的,并且在循环中重复调用流读/写比调用 File.Copy() 的一个本机复制文件成本更高会做的。 @Steve:你是对的,看看我的回复。 【参考方案1】:

你永远无法用自己的代码做如此重要的事情来击败操作系统,即使你用汇编程序精心制作它也是如此。

如果您需要确保您的操作以最佳性能发生并且您想要混合和匹配各种来源,那么您将需要创建一个描述资源位置的类型。然后,您创建一个 API,该 API 具有诸如 Copy 之类的函数,它采用两种此类类型,并检查了这两种类型的描述,然后选择性能最佳的复制机制。例如,在确定这两个位置都是 windows 文件位置后,您将选择 File.Copy 或者如果源是 windows 文件但目标是 HTTP POST,则它使用 WebRequest。

【讨论】:

【参考方案2】:

除尘反射器我们可以看到 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);

And here is the documentation for CopyFile

【讨论】:

【参考方案3】:

Mark Russinovich 将是这方面的权威。

他在他的blog 上写了一个条目Inside Vista SP1 File Copy Improvements,它总结了通过 Vista SP1 实现的 Windows 最新状态。

我半知半解的猜测是 File.Copy 在最多的情况下将是最强大的。当然,这并不意味着在某些特定的极端情况下,您自己的代码可能会胜过它...

【讨论】:

【参考方案4】:

File.Copy 是围绕CopyFile Win32 函数构建的,该函数引起了 MS 团队的大量关注(请记住这个与 Vista 相关的关于复制性能缓慢的线程)。

提高方法性能的几个线索:

    就像许多人之前所说的,从您的循环中删除 Flush 方法。你根本不需要它。 增加缓冲区可能会有所帮助,但仅限于文件到文件操作、网络共享或 ftp 服务器,这反而会减慢速度。 60 * 1024 是理想的网络共享,至少在 vista 之前是这样。在大多数情况下,对于 ftp 32k 就足够了。 通过提供缓存策略(在您的情况下为顺序读取和写入)来帮助 os,使用带有 FileOptions 参数 (SequentalScan) 的 FileStream 构造函数覆盖。 您可以使用异步模式加快复制速度(尤其适用于网络到文件的情况),但不要为此使用线程,而是使用重叠 io(.net 中的 BeginRead、EndRead、BeginWrite、EndWrite),以及不要忘记在 FileStream 构造函数中设置异步选项(参见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);

【讨论】:

【参考方案5】:

这是一个类似的答案

How do I copy the contents of one stream to another?

您的主要问题是对 Flush() 的调用,这会将您的性能绑定到 I/O 的速度。

【讨论】:

【参考方案6】:

三个变化将显着提高性能:

    增加缓冲区大小,试试 1MB(好吧——只是实验) 打开 fileStream 后,调用 fileStream.SetLength(inStream.Length) 以在磁盘上预先分配整个块(仅当 inStream 可查找时才有效) 删除 fileStream.Flush() - 它是多余的,可能对性能的影响最大,因为它会阻塞直到刷新完成。无论如何,流都会在 dispose 时被刷新。

在我尝试的实验中,这似乎快了大约 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】:

尝试移除 Flush 调用,并将其移到循环之外。

有时操作系统最清楚何时刷新 IO。它允许它更好地使用其内部缓冲区。

【讨论】:

我也不认为复制操作涉及多线程,我个人认为这是个坏主意。这意味着为每个复制操作创建一个线程,据说这比仅使用流更昂贵.. @aviadbenov:确实,创建我们自己的线程来处理 IO 操作是矫枉过正。但是,.NET 专门为此目的维护了一个线程池。正确使用异步 IO 调用可以让我们使用这些线程,而无需自己创建和销毁它们。 @Anthony:你说的是真的,但也很危险。如果有很多线程在复制文件,那么线程池本身就会成为复制操作的瓶颈!【参考方案8】:

突出的一点是您正在读取一个块,写入该块,读取另一个块等等。

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

尝试在一个线程中读取并在另一个线程中写入。您将需要协调线程,以便写入线程在读取线程完成填充缓冲区之前不会开始写掉缓冲区。您可以通过拥有两个缓冲区来解决这个问题,一个正在读取而另一个正在写入,以及一个指示当前正在将哪个缓冲区用于哪个目的的标志。

【讨论】:

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

以上是关于File.Copy 与手动 FileStream.Write 用于复制文件的主要内容,如果未能解决你的问题,请参考以下文章

FileStream 的 FlushAsync 方法在 .NET Framework 与 .NET Core 行为的不同

FileStream 的 FlushAsync 方法在 .NET Framework 与 .NET Core 行为的不同

FileStream 的 FlushAsync 方法在 .NET Framework 与 .NET Core 行为的不同

FileStream 的 FlushAsync 方法在 .NET Framework 与 .NET Core 行为的不同

FileStream类的使用

对FileStream的几种属性和方法认识