为什么Async版本比单线程版本慢?

时间:2015-10-20 13:38:10

标签: asynchronous f#

我正在使用XmlReader阅读一个大型XML文件,并正在通过Async& amp;流水线。以下对Async世界的初步尝试表明,Async版本(此时所有意图和目的都相当于同步版本)要慢得多。 为什么会这样?我所做的一切都包含在"正常"异步块中的代码,并使用Async.RunSynchronously

进行调用

代码

open System
open System.IO.Compression  // support assembly required + FileSystem
open System.Xml             // support assembly required


let readerNormal (reader:XmlReader) = 
    let temp = ResizeArray<string>()
    while reader.Read() do
        ()
    temp

let readerAsync1 (reader:XmlReader) = 
    async{
        let temp = ResizeArray<string>()
        while reader.Read() do
            ()
        return temp
    }

let readerAsync2 (reader:XmlReader) = 
    async{
        while reader.Read() do
            ()
    }

[<EntryPoint>]
let main argv = 

    let path = @"C:\Temp\LargeTest1000.xlsx"
    use zipArchive = ZipFile.OpenRead path
    let sheetZipEntry = zipArchive.GetEntry(@"xl/worksheets/sheet1.xml")

    let stopwatch = System.Diagnostics.Stopwatch()
    stopwatch.Start()
    let sheetStream = sheetZipEntry.Open()  // again
    use reader = XmlReader.Create(sheetStream)
    let temp1 = readerNormal reader
    stopwatch.Stop()
    printfn "%A" stopwatch.Elapsed

    System.GC.Collect()

    let stopwatch = System.Diagnostics.Stopwatch()
    stopwatch.Start()
    let sheetStream = sheetZipEntry.Open()  // again
    use reader = XmlReader.Create(sheetStream)
    let temp1 = readerAsync1 reader |> Async.RunSynchronously
    stopwatch.Stop()
    printfn "%A" stopwatch.Elapsed

    System.GC.Collect()

    let stopwatch = System.Diagnostics.Stopwatch()
    stopwatch.Start()
    let sheetStream = sheetZipEntry.Open()  // again
    use reader = XmlReader.Create(sheetStream)
    readerAsync2 reader |> Async.RunSynchronously
    stopwatch.Stop()
    printfn "%A" stopwatch.Elapsed

    printfn "DONE"
    System.Console.ReadLine() |> ignore
    0 // return an integer exit code

INFO

  1. 我知道上面的异步代码没有做任何实际的异步工作 - 我试图在这里确定的是简单地使它成为异步的开销

  2. 我不希望它变得更快只是因为我把它包裹在Async中。我的问题恰恰相反:为什么戏剧性(恕我直言)放缓。

  3. 的时间设置

    下面的评论正确地指出我应该为各种大小的数据集提供时间,这隐含地导致我在第一个实例中提出这个问题。

    以下有时基于小型和大型数据集。虽然绝对值不太有意义,但相对性很有意思:

    30个元素(小数据集)

      

    正常:00:00:00.0006994

         

    Async1:00:00:00.0036529

         

    Async2:00:00:00.0014863

    (慢很多但可能表示异步设置成本 - 这是预期的)

    150万元素

      

    正常:00:00:01.5749734

         

    Async1:00:00:03.3942754

         

    Async2:00:00:03.3760785

    (慢2倍。感到惊讶的是,随着数据集变大,时间差异没有摊销。如果是这种情况,那么流水线/并行化只能提高性能,如果你有两个以上的核心 - 超过开销我无法解释......)

2 个答案:

答案 0 :(得分:6)

没有异步工作要做。实际上,你得到的只是管理费用而且没有任何好处。 async {}并不意味着“大括号中的所有东西突然变得异步”。它只是意味着你有一种使用异步代码的简化方法 - 但你永远不会调用一个异步函数!

另外,“异步”并不一定意味着“并行”,并且它不一定涉及多个线程。例如,当你执行一个异步请求来读取一个文件(你在这里做)时,这意味着操作系统被告知你想要做什么,以及你应该如何通知完成时。当您使用RunSynchronously运行这样的代码时,您只是在发布异步文件请求时阻塞一个线程 - 这种情况与首先使用同步文件请求完全相同。

当您执行RunSynchronously时,您首先抛弃任何使用异步代码的理由。你仍在使用一个线程,你只是同时阻止了另一个线程 - 而不是保存线程,你浪费一个,然后添加另一个来完成真正的工作。

修改

好的,我用最小的例子进行了调查,我得到了一些观察。

  1. 对于分析器而言,差异绝对是残酷的 - 非异步版本稍慢(最多2倍),但异步版本永远不会结束。似乎大量的分配正在进行 - 然而,当我打破分析器时,我可以看到非异步版本(在4秒内运行)进行了十万次分配(~20 MiB),而异步版本(运行超过10分钟)仅仅数千。也许内存分析器与F#异步交互不良? CPU时间分析器没有此问题。
  2. 两种情况下生成的IL非常不同。最重要的是,即使我们的async代码实际上并没有异步执行任何操作,它也会创建大量的异步构建器帮助程序,通过代码进行大量(异步)Delay调用,然后直接进入荒谬的领域,循环的每次迭代都是一个额外的方法调用,包括设置一个辅助对象。
  3. 显然,F#会自动将while转换为异步while。现在,考虑到压缩的xslt数据通常是多么好,那些Read操作中涉及的I / O非常少,因此开销绝对占主导地位 - 并且因为“循环”的每次迭代都有自己的设置成本,开销与数据量成比例。

    虽然这主要是while实际上没有做任何事情造成的,但这显然意味着您需要注意选择async的内容,并且您需要避免在Read中使用它CPU时间占主导地位的情况(在这种情况下 - 毕竟,异步和非异步情况在实践中几乎都是100%的CPU任务)。 Parallel.For一次读取一个节点这一事实进一步恶化 - 即使在一个非压缩的大型xml文件中也是相对微不足道的。开销绝对占主导地位。实际上,这类似于将sum += iXmlReader.Read这样的主体一起使用 - 每次迭代的设置成本绝对使任何实际工作相形见绌。

    CPU分析使这一点变得相当明显 - 两个最耗费工作量的方法是:

    1. Thread::intermediateThreadProc(预期)
    2. async - 也称为“此代码在线程池线程上运行”。像这样的无操作代码中的开销大约是100% - yikes。显然,即使在任何地方都没有真正的异步性,回调也永远不会同步运行。循环帖子的每次迭代都可以工作到新的线程池线程。
    3. 吸取的教训是什么?可能类似于“如果循环体很少工作,不要在import tkinter as tk from tkinter.messagebox import showerror tk.Tk().withdraw() #Hide window that appears with message showerror('Title', 'Content') #display message 中使用循环”。循环的每次迭代都会产生开销。哎哟。

答案 1 :(得分:2)

异步代码并没有神奇地使您的代码更快。正如您所发现的那样,它会使隔离代码变慢,因为管理异步会产生开销。

Async的主要目的是使输入/输出代码更有效。

如果您直接调用&#39; slow&#39 ;,阻止I / O操作,则会阻止该线程,直到操作返回。

如果您反而异步调用该慢速操作,它可能会释放线程以执行其他操作。它确实需要一个底层实现,它不是线程绑定的,而是使用另一种机制来接收响应。 I / O完成端口可以是这样一种机制。

现在,如果你并行运行很多异步代码 ,它可能会比尝试并行运行阻塞实现更快,因为异步版本使用的资源更少(线程更少) =更少的记忆)。