为什么这个文件复制方法变慢了

本文关键字:方法 复制 文件 为什么 | 更新日期: 2023-09-27 18:18:04

我正在使用代码将文件从一个位置复制到另一个位置,同时动态生成校验和。对于小文件,代码功能正常,但对于大文件,例如3.8GB的文件,它的行为很奇怪:在大约1gb复制后,它突然变慢了,很快,然后越来越慢(例如,在达到1gb之前,我观察到每秒复制大约2%-4%的文件,然后当达到1gb时,它需要大约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上的总内存使用,包括当该方法运行时,总计为56%(对于在该pc上运行的所有应用程序))。PC机总内存为8gb。

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

作为测试后的更新评论建议:当我复制并通过令牌取消它并删除文件(所有在减速开始之后),并立即开始第二次复制过程,它和我取消它时的另一个一样慢(所以减速已经在1 GB之前开始了)。但是当我在删除完成后进行第二次复制时,它正常启动,只在1gb时减慢。

同样刷新目标流在这里没有区别。

对于减慢速度的拷贝,开始时大约是每秒84MB,在1gb时减慢到每秒14MB。

作为这个问题的一部分(不确定作为评论是否更好):是否有可能这不是c#相关的问题,而是"完全"来自操作系统的缓存机制的问题?(如果有的话,可以在那里做些什么)

正如建议的那样,我寻找操作系统的写缓存,并让性能监视器运行。结果:

  • 不同的源硬盘和源桌面有相同的结果,也有相同的减速时刻
  • 操作系统(目标)的写缓存被禁用
  • 目标所在服务器上的性能监控没有显示出什么重要的(写队列长度只有一次为4,一次为2,写时间/空闲时间和写时间/秒没有显示任何表明100%使用缓存或其他东西)。

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

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

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

作为一个补充:总的来说,3gb文件的性能约为37mb/s(第一个GB为84mb,另一个GB为14mb)。

为什么这个文件复制方法变慢了

只是猜测,但我觉得值得一试。可能与文件系统的空间分配算法有关。起初,它不能预测文件的大小。它分配了一个空间,但是过了一段时间(在您的例子中是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
        }
    }
}

您可以看到操作系统写缓存对磁盘IO的影响。您可以禁用此硬盘驱动器-获得您的驱动器属性(不是驱动器号)。右键单击驱动器号,检查硬件选项卡,选择磁盘,单击属性,单击"更改设置",然后在Policies选项卡中显示写缓存策略。重新启动以确保)。

编辑1。

ok,不是文件系统缓存io。如果在网络上启用巨型帧会发生什么?您需要在客户端和服务器网络驱动程序设置上执行此操作,可能还需要在交换机上执行此操作(取决于交换机)。吞吐量应该会增加。有可能是操作系统限制了网络带宽——试着在你的网络驱动程序设置中禁用QoS服务(我认为只有客户端,但两边都这样做永远不会有坏处)

然后你可以把wireshark打开,看看哪些SMB数据包正在通过网络发送,以及在减速过渡时发生了什么。

您遇到的问题可能与硬件有关,而与c#无关。当您在删除后开始第二次复制操作时,可能存在一个缓存,它仍然是满的。根据你的磁盘类型,hd/ssd/hybrid/raid,你可以得到非常不同的结果。为了进一步调查,你应该安装一些低级监控工具,并向hd供应商询问读/写缓存的规格。

我非常同意这个帖子的其他答案;你的问题可能不在c#代码中。
可能产生这种行为的原因有很多,其中一些已经在下面的回答和评论中列出了。为了找出你的问题的原因,让我们列一个清单,逐一排除它的任务。

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

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

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

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

虽然我不太明白为什么要制作如此复杂的复制算法,具有如此大的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;内存12gb

虽然在我的测试中,我们可以看到超过1 GB的速度变慢了,但是,不像Thomas所显示的那样明显(84 MB/s vs 14 MB/s)。我们还应该考虑硬盘驱动器的碎片情况可能会贡献显著的变量。更科学的测试应该在碎片化磁盘中构建,大小文件位于相似的半径范围内。

使用文件。Copy给出类似的结果,可能是因为File。Copy使用和我类似的算法。像Windows这样的现代操作系统是相当聪明的,.NET framework和Windows的默认设置通常会给你最好的性能;除非您非常了解操作系统和目标系统,否则即使使用过于复杂的算法也很难改变设置,从而获得更好和一致的性能。

因此,复杂的算法似乎不能很好地处理硬盘的旋转特性。虽然我听说有些糟糕的硬盘在处理大文件时表现不佳,但是,您为什么不在其他具有不同类型硬盘的计算机上测试您的程序/算法呢?如果你的程序在不同的驱动器驱动器上有一致的奇怪的性能,低端或高端,那么你可以肯定是算法有问题。

尽管如此,硬件架构确实对整体性能有显著的影响,但由于基本旋转特性的限制,不能显著区分小文件和大文件。例如,在RAID上或在两个物理硬盘驱动器之间进行复制时,通过异步读/写甚至并发,特定算法可能会显著提高性能。但那是另一个话题了。

相关文章: