【问题标题】:Why is this file copying method slowing down为什么这种文件复制方法变慢了
【发布时间】:2014-08-14 13:59:17
【问题描述】:

我正在使用代码将文件从一个位置复制到另一个位置,同时动态生成校验和。对于小文件,代码可以正常运行,但对于大文件,例如 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# 相关的问题,而是“完全”来自操作系统的缓存机制问题吗? (如果可以的话,可以在那里做点什么)

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

  • 不同的源硬盘和源桌面有相同的结果,也有相同的减速时刻
  • 操作系统(目标)中的写入缓存已禁用
  • 目标所在服务器上的性能监控显示没有任何意义(写入队列长度只有一次在 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)。

【问题讨论】:

  • 您在文件复制期间是否跟踪了内存使用情况?
  • yepp 内存在方法启动时确实会增加一点(正如预期的那样,由于缓冲区很大),但随后保持稳定并且不会以任何形式奇怪地增加。
  • Thread.Sleep() 似乎很奇怪。为什么?
  • 为什么复制块只是为了写出它——为什么不直接从 ReadBuffer 中写出呢?如果您注释掉 MD5 部分,它是否也会变慢?
  • 我是认真的。使用网络搜索了解更多信息。您可以将您的线程标记为执行低优先级 IO。如果没有人在正常优先级与您竞争,您将获得完整的 IO 性能。否则你会被打断,需要等待。

标签: c# file-io


【解决方案1】:

只是一个猜测,但我觉得值得一试。可能与文件系统的空间分配算法有关。起初它无法预测文件的大小。它分配了一个空间,但过了一段时间(在你的情况下为 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.3 GB 时它会显着减慢。
  • 很遗憾对这种现象没有影响。
【解决方案2】:

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

编辑 1.

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

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

【讨论】:

  • 我已经更新了关于我在检查您关于写入缓存的建议时看到的结果的问题,以及对不同客户端计算机的另一项测试:从 OS 端写入缓存 wsa 已停用整个时间。
【解决方案3】:

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

【讨论】:

  • 我已经用系统管理员在此处进行的性能测试结果更新了问题。奇怪的是,写入缓存长度或磁盘操作没有异常结果(尽管所有都确实指向某种缓存问题,因为我看到了类似的现象结果在这里被质疑:superuser.com/questions/315134/… 并且它也与缓存相关)跨度>
【解决方案4】:

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

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

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

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

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

【讨论】:

    【解决方案5】:

    虽然我不太明白你为什么要设计如此复杂的复制算法,具有如此大的 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;视窗 7 64 位;英特尔至强 2.67GHz;内存 12 GB

    在我的测试中,我们可以看到超过 1 GB 的速度较慢,但​​是,并没有 Thomas 显示的那么显着(84 MB/s 对 14 MB/s)。我们还应该考虑硬盘驱动器的碎片情况可能会产生重大变量。更科学的测试应该在碎片整理的磁盘中构建,大小文件位于相似的半径范围内。

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

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

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

    【讨论】:

    • 我担心你在我的帖子中忽略了一点。我一开始有 84 MB/s,然后下降到 14 MB/s,但只有在 1 GB 增加之后)。因此,总共 ((14*2GB + 84 * 1GB) / 3 = 37.3 GB / 秒,因此它与您的例程具有相同的总结果(因为 14 MB/秒用于剩余的 2 GB)。不过有一点好您的回答似乎与操作系统/硬件相关或与 C# 相关(尽管据我所知,C# 直接使用操作系统中的 dll 进行文件访问)。
    • 还有一个有趣的结果,当我使用 Windows 资源管理器复制同一个文件时,它需要 5 分钟以上,速率为 11 MB/s
    • .NET 在这方面除了对 Windows API 的本机调用外,还提供了良好的内存管理。 Windows 和 CLR 的开发人员在无需应用程序开发人员编写复杂算法的情况下,为使应用程序获得良好的性能付出了很多努力。 IO 操作由 OS 和 CLR 密切监控,您的智能算法可能会被此类监控规避。如您所见,您的智能长代码和我的转储和短代码对于大文件的性能基本相似。
    • 除非您的应用程序始终专门用于复制大文件,否则我们观察到的情况可能是设计良好的,因为系统的整体性能应该优先读取和写入“小文件”。
    • 或大文件的头部。
    猜你喜欢
    • 1970-01-01
    • 2018-01-20
    • 1970-01-01
    • 2014-03-01
    • 1970-01-01
    • 2015-04-14
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多