为什么使用.NET async await复制文件比同步的File.Copy()调用要消耗更多的CPU?

8
下面的代码为什么会出现如下结果: .NET async file copy
public static class Program
{
    public static void Main(params string[] args)
    {
        var sourceFileName = @"C:\Users\ehoua\Desktop\Stuff\800MFile.exe";
        var destinationFileName = sourceFileName + ".bak";

        FileCopyAsync(sourceFileName, destinationFileName);

        // The line below is actually faster and a lot less CPU-consuming
        // File.Copy(sourceFileName, destinationFileName, true);

        Console.ReadKey();
    }

    public static async void FileCopyAsync(string sourceFileName, string destinationFileName, int bufferSize = 0x1000, CancellationToken cancellationToken = default(CancellationToken))
    {
        using (var sourceFile = File.OpenRead(sourceFileName))
        {
            using (var destinationFile = File.OpenWrite(destinationFileName))
            {
                Console.WriteLine($"Copying {sourceFileName} to {destinationFileName}...");
                await sourceFile.CopyToAsync(destinationFile, bufferSize, cancellationToken);
                Console.WriteLine("Done");
            }
        }
    }
}

在使用File.Copy()时,https://msdn.microsoft.com/en-us/library/system.io.file.copy(v=vs.110).aspx的CPU消耗要少得多: 同步文件复制 那么,对于文件复制的目的,仍然有使用async/await的真正意义吗?
我认为节省线程用于复制可能是值得的,但是File.Copy函数似乎在CPU%方面轻松获胜。 有人会认为这是因为硬件支持了真正的DMA,但是,我是否做了任何事情来破坏性能?或者是否有任何可以改进我的异步方法的CPU使用率的方法?

1
这可能与File.Copy实现的某些底层方式与您直接使用File.OpenReadFile.OpenWrite的流式方法之间的差异有关。我怀疑这与同步与异步无关。 - Abion47
3
就像我之前说的,我怀疑这与它是异步的没有任何关系,而与它执行复制操作的方式有关。你的异步方法从一个流复制到另一个流,而同步方法使用File.Copy,它封装了本机Win32操作,采用更低层次的方法。请参见https://dev59.com/yHM_5IYBdhLWcg3wvV5w。 - Abion47
11
你的问题是:我让编译器生成代码,以便在等待IO操作时能利用CPU,为什么这样做会得到更高的CPU利用率?当你这样提问时,问题其实已经回答了自己,不是吗?你认为异步是用来做什么的?它的作用是在等待IO时增加CPU的使用量。记住,高CPU利用率是好事。人们说它很糟糕,但高CPU利用率是极好的。机器的所有者为那个CPU付钱,每毫秒空闲都是资源的浪费。 - Eric Lippert
2
也许这里还有其他问题;正如其他人所说,可能你正在受到病毒检查器或其他东西的短暂影响。特别是当你的程序在等待时似乎没有做任何事情。但总的来说,当使用异步操作时,你应该期望看到更好、更高的 CPU 利用率。不要让 CPU 停滞不前;让它继续工作以解决 CPU 密集型问题。 - Eric Lippert
3
是的,正如我在评论中提到的,我们希望CPU能够高效地完成计算机密集型工作。尽管我们很挑剔,但需要注意的是,在Windows上,一个.NET线程需要1MB的虚拟地址空间,操作系统足够聪明,不会将其映射到内存中,直到必要时才这样做。因此,大部分时间最好将其视为交换文件(swap file)中的1MB,而不是RAM。 - Eric Lippert
显示剩余11条评论
3个回答

9
这些性能数据相当荒谬。你根本没有测量到你认为的东西。对于缓存文件数据,这不应该需要更多的时间,只需进行简单的内存到内存复制,就像 File.Copy() 一样。在具有良好 DDR3 RAM 的机器上运行速度约为 ~35 GB/秒,因此不需要几十毫秒。即使文件未被缓存或机器没有足够的 RAM,你也无法获得这种CPU负载,因为你的代码将被阻塞等待磁盘。

你实际上看到的是你安装的反恶意软件的性能。它总是在看到程序操纵可执行文件时变得很紧张。

很容易验证,禁用它或进行排除并重新尝试。


关于VS的“恶意软件”我们已经达成了共识,但我想指出,任务管理器仍然显示CPU%有一定增长(而且当使用async/await时,我的廉价笔记本电脑的廉价风扇会更加吃力)。再次肯定地说,实现File.Copy的方式确实更好,但是通过使用isAsync参数,我设法将异步实现的CPU%降低了约75%。我将前往GitHub上的code corefx查看,因为这对幕后发生的事情仍然有点模糊。无论如何,感谢你为测试环境带来的光明。 - Natalie Perret
2
你实际上看到的是已安装的反恶意软件产品的性能。您能否澄清一下您的意思?您是说反恶意软件产品正在使用大量CPU,这在Visual Studio中显示出来吗?还是您是说其他程序导致@EhouarnPerret的程序本身使用更多的CPU?我刚刚打开了Prime95,在任务管理器中将我的8个核心全部占满了100%,但Visual Studio仍然显示约13%的CPU使用率,与任务管理器为我的1个“ConsoleApplication”进程显示的数字相同。 - Quantic
2
通常情况下,您不会看到反恶意软件引起的开销,因为您没有专业观察程序正在执行的工具。请勿攻击信息传递者 :) 如果您有其他不信任分析器的情况,请单击“提问”按钮。 - Hans Passant

6

File.OpenRead(sourceFileName) 等价于 new FileStream(sourceFileName, FileMode.Open, FileAccess.Read, FileShare.Read),而后者等价于 public FileStream(sourceFileName, FileMode.Open, FileAccess.Read, FileShare.Read, 4096, false),其中使用了false来表示非异步I/O。对于File.OpenWrite同理。

因此,任何XXXAsync操作都不会使用异步I/O,而是使用线程池线程来模拟。这样既没有享受异步I/O的好处,还浪费至少一个线程,相当于本来想避免的阻塞I/O引入了额外的线程。一般情况下,单独使用异步操作性能会比同步略低(异步通常会为更好的可扩展性而牺牲一次性速度),但我肯定会期望此方法的表现几乎不如在Task.Run()中包装整个操作。

我仍然不认为它会变得很糟糕,但可能是反恶意软件担心写exe文件。

如果复制的是非exe文件并使用异步流,则有望获得更好的结果。


实际上,当我评论Hans的答案时提到了isAsync参数一旦设置为true,CPU%大大降低,但速度似乎要慢得多(这花费了我一段时间,因为我一开始没有对文件复制速度进行基准测试,但似乎File.Copy明显比async await快2-3倍,但同样需要保持警惕)。我明天会更新帖子以进行适当的基准测试。我接受你的答案,因为它与Eric Lippert的评论一起非常有意义。 - Natalie Perret
3
如果异步复制比同步慢上几倍,我不会感到恐慌。当我想要更高的单次操作吞吐量时,我不会使用它;而是在我同时想做其他事情时使用它,或者在Web应用程序中使用它,此时我更关心同时提供多少服务以及能够快速提供多少服务,而不是关心每个请求的响应速度有多快。 - Jon Hanna

3

File.Copy 看起来是一次性复制整个文件。使用 FileStreams 时,缺省缓冲区大小为 4096 字节,所以每次会拷贝4kb。

我编写了自己的 async 函数,它不仅仅是简单的文件复制(它还匹配文件大小并进行清理),但下面是通过在一个 50mbps 的宽带连接上跨 VPN 复制文件进行基准测试的结果。

当使用默认的 4096 字节时,我的 async 文件复制:

Copy of 52 files via CopyFileAsync() took 5.6 minutes

对比

File.Copy拥有

Copy of 52 files via File.Copy() took 24 secs, 367 ms

当我将缓冲区大小增加到64KB时,我得到了以下结果

Copy of 52 files via CopyFileAsync() took 39 secs, 407 ms

底线是4096默认缓冲区大小对于现代硬件来说太小了,这就是为什么通过流拷贝会如此缓慢。您需要根据将要使用的硬件进行基准测试,以确定最佳缓冲区大小,但一般来说,64K对于互联网上的网络流量相当合适。


请注意,您机器上的最佳缓冲区大小可能不适用于其他人的机器。另请参阅使用FileInputStream时如何确定理想的缓冲区大小? - Brian

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