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

9

我正在使用代码将文件从一个位置复制到另一个位置,并在复制过程中生成校验和。对于小文件,该代码可以正常运行,但是对于大文件(例如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的总内存为8 GB。
应用程序本身是32位的(本身占用约300 MB的内存),所用的框架是4.5。
根据评论建议的测试更新:当我复制并通过令牌取消它并删除文件(所有这些都发生在减速开始之后),并立即开始第二个复制过程时,它与时间我取消它时的另一个复制一样慢(因此,在1 GB之前就已经开始减速)。但是,当我在删除完成后进行第二次复制时,它开始正常工作,并且只在1 GB处减速。
同时刷新目标流不会有任何区别。
对于减速而言,最初的复制速度约为84 MB /秒,到1 GB时减速至约14 MB /秒。
作为这个问题的一部分(不确定是否更适合作为评论):这可能不是C#相关的问题,而是OS缓存机制的“唯一”问题吗? (如果是这样,是否可以在那里做些事情)
根据建议,我查找了OS的写入缓存,还让性能监视器运行。
结果:
不同的源硬盘和源桌面具有相同的结果,减速的时间也相同
OS中的写缓存(目标)已禁用
位于目标位置的服务器上的性能监视显示没有重要信息(写入队列长度仅一次为4,一次为2,写入时间/空闲时间以及每秒写入均未显示出使用缓存或其他东西的100%)
进一步的测试显示了以下行为:
- 如果通过在每次写入后进行200毫秒的Thread.Sleep来减慢复制本身,平均复制速率为30 MB / s,保持恒定 - 如果我改为在每传输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)。


4
为什么 Thread.Sleep() 看起来很奇怪? - David Heffernan
@DavidHeffernan 我添加了thread.sleep,因为我观察到在之前使用该程序时硬盘的使用率非常高。通过这种睡眠方式,我减少了其他方法访问硬盘的延迟,并使其他线程更容易完成工作(当时从stackoverflow上的另一个帖子中得到了这个想法,该帖子中有人遇到类似的问题,有一个文件操作几乎锁定了计算机)。 - Thomas
1
为什么你要复制整个块才写出来 - 为什么不直接从ReadBuffer中写出来呢?如果你注释掉MD5部分,它会减慢速度吗? - 500 - Internal Server Error
那个解决方案是错误的。你应该使用低优先级 I/O。 - David Heffernan
1
我说的就是这个意思。使用网络搜索以了解更多信息。您可以将线程标记为执行低优先级IO操作。如果没有人以正常优先级争夺资源,您将获得完整的IO性能。否则,您将被中断并需要等待。 - David Heffernan
显示剩余21条评论
5个回答

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-1.3 GB时,速度明显变慢。 - Thomas
很遗憾,这对现象没有任何影响。 - Thomas

1
您可能正在看到操作系统写缓存对磁盘IO的影响。您可以为硬盘禁用此功能-获取驱动器的属性(不是驱动器字母)。右键单击驱动器字母,检查硬件选项卡,选择磁盘,单击属性,单击“更改设置”,然后在策略选项卡上启用写缓存策略。重新启动以确保更改生效。
编辑1。
好的,不是文件系统缓存io。如果在网络上启用了jumbo帧,会发生什么?您需要在客户端和服务器网络驱动程序设置上执行此操作,并且还可能需要在交换机上执行此操作(取决于交换机)。吞吐量应该会增加。 操作系统可能会限制网络带宽-尝试在网络驱动程序设置中禁用QoS服务(我认为仅适用于客户端,但两侧都这样做从未有过错)。
然后,您可以将wireshark放置在上面,看看通过网络发送了哪些SMB数据包以及在减速转换时会发生什么。

我已经更新了关于我在检查您有关写缓存建议的结果以及使用不同客户端机器进行的另一个额外测试的问题:操作系统方面的写缓存一直处于停用状态。 - Thomas

0
我非常赞同其他回答这篇帖子的观点;你的问题可能不在于C#代码。
有很多原因可能会导致这种行为,其中一些已经在下面的回答和评论中列出。为了确定问题的原因,让我们制作一个清单,逐个排除其任务。

让我们从相同的源和目标位置使用Windows复制功能,复制你正在工作的同一文件,并测试你的C#代码。我们将观察带宽速度。

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

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

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


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

我已经更新了问题,并附上了系统管理员进行的性能测试结果。奇怪的是,写缓存长度或磁盘操作并没有出现异常结果(尽管所有指向某种缓存问题,因为我在这里看到了类似的现象:http://superuser.com/questions/315134/why-does-my-flash-drive-speed-slow-down-when-copying,那里也与缓存有关)。 - Thomas

0

虽然我不太明白为什么你要编写这样复杂的复制算法,使用如此大的读写缓冲区、校验和和奇怪的休眠。我用了所有默认设置的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,发布版,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上复制或在两个物理硬盘之间复制,特定算法可能通过异步读/写甚至并发显著提高性能。但那是另一个话题了。


我担心你在我的帖子中忽略了一个要点。我一开始有84 MB/s,然后它下降到14 MB/s,但只有在上传1 GB后才会下降。因此,总共是((14*2GB + 84 * 1GB) / 3 = 37.3 GB /秒,因此它与您的例程具有相同的总结果(因为14 MB/秒是剩余2 GB的速度)。不过,你的回答中有一个好观点,似乎这要么是固有的操作系统/硬件相关,要么是C#相关(尽我所知,C#直接使用来自操作系统的dll进行文件访问)。 - Thomas
当我使用Windows资源管理器复制同一文件时,速率为11 MB/s,但需要超过5分钟的时间,这也是一个有趣的结果。 - Thomas
在这方面,.NET 除了调用 Windows API 还能很好地管理内存。Windows 和 CLR 的开发人员为了让应用程序获得良好的性能而做出了很多努力,使应用程序开发人员不需要编写复杂的算法。IO 操作由操作系统和 CLR 密切监控,这些监控可能会规避您的智能算法。正如您所看到的,对于大文件,您的智能长代码和我的简单短代码基本上具有相似的性能。 - ZZZ
除非您的应用程序专门用于始终复制大文件,否则我们观察到的情况很可能是出于良好设计考虑的,以提高系统的整体性能,这应该优先考虑读写“小文件”。 - ZZZ
或者大文件的头部分。 - ZZZ

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