为什么这种文件复制方法会变慢

时间:2014-08-14 13:59:18

标签: c# file-io

我正在使用代码将文件从一个位置复制到另一个位置,同时动态生成校验和。对于小文件,代码正常运行,但对于大文件,例如3.8GB文件,它表现得很奇怪:在大约1 GB复制后,它突然减速很快,然后越来越慢(例如在达到1 GB之前我观察到大约每秒复制2%-4%的文件,然后当达到1 GB时,每个文件的文件大约需要4-6秒。)

 int bytesRead = 0;
 int bytesInWriteBuffer = 0;
 byte[] readBuffer = new byte[1638400];
 byte[] writeBuffer = new byte[4915200];
 MD5 md5Handler = new MD5CryptoServiceProvider();
 using (FileStream sourceStream = File.Open(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
{
    md5Handler.TransformBlock(readBuffer, 0, bytesRead, null, 0);
    FileStream destinationStream = File.Create(storageFileName);
    while (bytesRead = sourceStream.Read(readBuffer, 0, readBuffer.Length))
    {
        Buffer.BlockCopy(readBuffer, 0, writeBuffer, bytesInWriteBuffer, bytesRead);
        bytesInWriteBuffer += bytesRead
        if (bytesInWriteBuffer >= 4915200)
        {
             destinationStream.Write(writeBuffer, 0, bytesInWriteBuffer);
             bytesInWriteBuffer = 0;
             Thread.Sleep(50);
        }
    }
}   

正如评论中所提到的:没有可观察到的内存泄漏。在方法开始时内存使用量增加,然后保持稳定(运行它的PC上的总内存使用量,包括运行mthod的总时间为56%(对于在该PC上运行的所有应用程序))。 PC的总内存为8 GB。

应用程序本身是32位(本身占用大约300 MB的内存),使用的框架是4.5。

作为测试后的更新评论提示:当我制作副本并通过令牌取消它并删除文件(所有这些都在减速开始后),并立即开始第二个复制过程,它的速度和另一个是在我取消它的时候(所以减速开始时已经在1 GB之前)。但是当我在删除完成后制作第二个副本时,它会正常启动并且只会减慢到1 GB。

同样刷新目标流也没有区别。

对于速度减慢,副本最初大约为每秒84MB,而在1 GB时速度减慢到大约每秒14MB。

作为这个问题的一部分(不确定作为评论是否更好):这可能不是C#相关的问题,而是"仅仅#34;操作系统的缓存机制问题? (如果可以的话,可以在那里完成)

根据建议,我查找了操作系统的writecache,并让性能监视器运行。 结果:

  • 不同的源硬盘驱动器和源桌面具有相同的结果,也是同样的减速时刻
  • 禁用OS(目标)中的写入缓存
  • 目标所在的服务器上的性能监控没有显示任何重要性(写入队列长度仅为4次,一次为2次,写入时间/空闲时间,写入/秒次显示没有任何暗示100%使用缓存或别的东西)。

进一步测试显示以下行为:

  • 如果通过在每次写入后执行200毫秒Thread.Sleep来减慢复制本身,则平均复制速率为30 MB /秒,这是恒定的
  • 如果我在每500 MB或800 MB传输后延迟5秒(Thread.Sleep),则会再次减速,等待不会发生任何变化。
  • 如果我更改位置以使源和目标位于我的本地硬盘驱动器上(通常目标位于网络文件夹中),则速率恒定为50 MB / s,而读取时间为100%且瓶颈在那里,写时间低于100%。
  • 网络传输监控未显示任何意外
  • 将3 GB文件从同一源复制到同一目标时,Windows资源管理器的传输速率为11 MB / s(因此,尽管总体上发生了减速,但C#复制方法比Windows资源管理器复制速度快)

进一步行为:

  • 根据监控事项,目标驱动器有一个恒定的流(因此没有快速的第一部分和减速,但目的地不断以相同的速度接收字节)。

作为对此的补充:总共3 GB文件的性能约为37 MB / s(第一个GB为84 MB,另一个GB为14 MB)。

5 个答案:

答案 0 :(得分:4)

只是一个猜测,但我觉得值得一试。它可能与文件系统的空间分配算法有关。起初它无法预测文件的大小。它会分配一个空格,但过了一会儿(在你的情况下为1GB)它会到达边界。然后它可能会尝试移动邻居文件以创建连续存储。看看这个:https://superuser.com/a/274867/301925

为了确保,我建议您创建一个初始大小的文件,如下面的代码所示,并记录每个步骤中经过的时间。 (我没有可以尝试的环境,如果它包含语法错误则更正它)

int bytesRead = 0;
int bytesInWriteBuffer = 0;
byte[] readBuffer = new byte[1638400];
byte[] writeBuffer = new byte[4915200];
//MD5 md5Handler = new MD5CryptoServiceProvider(); exclude for now
Stopwatch stopwatch = new Stopwatch();
long fileSize = new FileInfo(filePath).Length;
using (FileStream sourceStream = File.Open(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
{
    //md5Handler.TransformBlock(readBuffer, 0, bytesRead, null, 0); exclude it for now
    stopwatch.Start();
    FileStream destinationStream = File.Create(storageFileName);
    stopwatch.Stop();
    Console.WriteLine("Create destination stream: " + stopwatch.ElapsedMilliseconds);

    stopwatch.Restart();
    // trick to give an initial size
    destinationStream.Seek(fileSize - 1, SeekOrigin.Begin);
    destinationStream.WriteByte(0);
    destinationStream.Flush();
    destinationStream.Seek(0, SeekOrigin.Begin);
    stopwatch.Stop();
    Console.WriteLine("Set initial size to destination stream: " + stopwatch.ElapsedMilliseconds);

    while (true)
    {
        stopwatch.Restart();
        bytesRead = sourceStream.Read(readBuffer, 0, readBuffer.Length);
        stopwatch.Stop();
        Console.WriteLine("Read " + bytesRead + " bytes: " + stopwatch.ElapsedMilliseconds);

        if(bytesRead <= 0)
            break;
        Buffer.BlockCopy(readBuffer, 0, writeBuffer, bytesInWriteBuffer, bytesRead);
        bytesInWriteBuffer += bytesRead;
        if (bytesInWriteBuffer >= 4915200)
        {
            stopwatch.Restart();
            destinationStream.Write(writeBuffer, 0, bytesInWriteBuffer);
            stopwatch.Stop();
            Console.WriteLine("Write " + bytesInWriteBuffer + " bytes: " + stopwatch.ElapsedMilliseconds);

            bytesInWriteBuffer = 0;
            //Thread.Sleep(50); exclude it for now
        }
    }
}

答案 1 :(得分:1)

您可能会看到操作系统写入缓存对磁盘IO的影响。您可以为硬盘驱动器禁用此功能 - 获取驱动器的属性(不是驱动器号。右键单击驱动器号,检查硬件选项卡,选择磁盘,单击属性,单击“更改设置”,然后写入缓存策略位于“策略”选项卡上。重新启动只是为了确保)。

编辑1。

好吧,不是文件系统缓存io。如果在网络上启用巨型帧会发生什么?您需要在客户端和服务器网络驱动程序设置上执行此操作,并且可能也需要在交换机上执行此操作(取决于交换机)。吞吐量应该增加。 操作系统可能会限制网络带宽 - 尝试在网络驱动程序设置中禁用QoS服务(我认为仅限客户端,但双方都不会受到伤害)

然后你可以坚持使用wireshark并查看通过网络发送的SMB数据包以及在减速过渡时会发生什么。

答案 2 :(得分:0)

您遇到的问题可能与硬件有关,而与c#无关。在删除后启动第二个复制操作时,可能存在一个仍然已满的缓存。根据您的磁盘类型,hd / ssd / hybrid / raid,您可能会得到非常不同的结果。为了进一步研究,您应该安装一些低级监视工具,并向您的高清供应商询问有关读/写缓存的规范。

答案 3 :(得分:0)

我非常同意这篇文章的其他答案;你的问题可能不在C#代码中 可能产生此类行为的原因有很多,其中一些已在下面的答案和评论中列出。为了找出问题的原因,让我们制作一份清单并逐一排除其任务。

让我们使用您的c#代码从使用c#代码测试的相同源和目标复制您正在处理的同一文件,但这次使用的是Windows副本。我们将观察带宽速度。

1-如果一切正常,没有减速
   **然后我们有一个C#编码问题(不太可能发生)

 2-如果观察到减速。我们可能有三种可能的情况:
   2.1-源或目的地可能存在磁盘问题:
   **为了排除这种可能性,您应该对源和目标磁盘进行一些测试;我建议使用此工具:
http://crystalmark.info/?lang=auto
并在此处发布结果。当我说磁盘问题我不一定意味着物理损坏。磁盘问题可能会影响阅读和写作。
   2.1-可能是网络问题       **应进行网络带宽测试    2.3-可能的操作系统缓存机制
      ** OS相关配置;许多建议已经在这个帖子中发布。

正如您所看到的,有很多原因可能会导致这种行为。我发布的是一个诊断树,它可以让你排除不太可能发生的情况,并专注于剩下的问题。

答案 4 :(得分:0)

虽然我不太理解你为什么会制作如此复杂的复制算法,但有如此大的r / w缓冲区,校验和以及奇怪的睡眠。我用BCL代码和常见的本地硬盘驱动器的所有默认设置编写了我自己的测试。

        static void Main(string[] args)
    {
        DateTime dt = DateTime.Now;
        long length=0;
        using (var source = new FileStream(args[0], FileMode.Open, FileAccess.Read))
        using (var dest  = new FileStream(args[1], FileMode.CreateNew, FileAccess.Write))
        {
            source.CopyTo(dest);//default buffer size 81920
            length=source.Length;
        };
        var span = (DateTime.Now-dt).TotalSeconds;
        Console.WriteLine(String.Format("Time: {0} seconds; speed: {1} byte/second", span, length/span));
    }

以下是我本地硬盘上的结果:

68 MB,  94 MB/s
80 MB,  94 MB/s
232 MB, 86
680 MB, 48
980 MB, 63
3.5 GB, 37 
5.9 GB, 36

平台:.NET 4.5,Release,AnyCPU; Windows 7 64位; Intel Xeon 2.67GHz;内存12 GB

虽然在我的测试中,我们可以看到速度较慢的1 GB以上,但不像Thomas所示的那样显着(84 MB / s vs 14 MB / s)。我们还应该考虑硬盘驱动器的碎片情况可能会带来重大变数。应在碎片化的磁盘中构建一个更科学的测试,文件小或大的半径相似。

使用File.Copy会得到类似的结果,可能是因为File.Copy使用了与我类似的算法。像Windows这样的现代操作系统非常智能,.NET Frameworking和Windows中的默认设置主要为您提供最佳性能;除非您非常深入地了解操作系统和目标系统,否则即使使用过于复杂的算法来扭曲设置也很难提供更好和一致的性能。

因此复杂的算法似乎不适用于硬盘的旋转性质。虽然我听说一些可怜的硬盘驱动器在大文件上表现不佳,但是,为什么不在其他具有不同类型硬盘的计算机上测试你的程序/算法呢?如果您的程序在不同的驱动器驱动器(低端或高端)上具有一致的奇怪性能,那么您可以确定它是具有该问题的算法。

尽管如此,硬件架构确实对整体性能产生了重大影响,但并没有明显区分小型和大型文件,而是出于基本旋转性质的限制。例如,在RAID或2个物理硬盘驱动器之间进行复制,特定算法可能会通过异步读/写甚至并发来显着提高性能。但那是另一个主题。