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

Sam*_*Sam 1 f# asynchronous

我正在使用XmlReader阅读一个大型XML文件,并正在通过异步和流水线技术探索潜在的性能改进.以下对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
Run Code Online (Sandbox Code Playgroud)

信息

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

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

的时间设置

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

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

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倍.感到惊讶的是,随着数据集变大,时间差异没有摊销.如果是这种情况,那么流水线操作/并行化只能提高性能,如果你有两个以上的内核 - 超过我可以承受的开销解释......)

Lua*_*aan 6

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

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

在你做的那一刻RunSynchronously,你抛弃任何理由首先使用异步代码.你还在使用单个线程,你只是同时阻止另​​一个线程 - 而不是保存线程,你浪费一个,然后添加另一个线程来做真正的工作.

编辑:

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

  1. 对于分析器而言,差异绝对是残酷的 - 非异步版本稍慢(最多2倍),但异步版本永远不会结束.似乎大量的分配正在进行 - 然而,当我打破分析器时,我可以看到非异步版本(在4秒内运行)进行了十万次分配(~20 MiB),而异步版本(运行超过10分钟)只需要数千个.也许内存分析器与F#异步交互不良?CPU时间分析器没有此问题.
  2. 对于这两种情况,生成的IL非常不同.最重要的是,即使我们的async代码实际上没有异步执行任何操作,它也会创建大量异步构建器助手,Delay通过代码进行大量(异步)调用,并进入完全荒谬的区域,循环的每次迭代都是额外的方法调用,包括辅助对象的设置.

显然,F#会自动转换while为异步while.现在,考虑到压缩xslt数据的通常情况,这些Read操作中涉及的I/O非常少,因此开销绝对占主导地位 - 并且由于"循环"的每次迭代都有自己的设置成本,因此开销会随着数据量的增加而变化.

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

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

  1. XmlReader.Read (预期)
  2. Thread::intermediateThreadProc - 也称为"此代码在线程池线程上运行".像这样的无操作代码中的开销约为100% - yikes.显然,即使在任何地方都没有真正的异步性,回调也不会同步运行.循环帖子的每次迭代都会工作到新的线程池线程.

吸取了教训?可能类似于" async如果循环体很少工作则不使用循环".循环的每次迭代都会产生开销.哎哟.