我正在使用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)
我知道上面的异步代码没有做任何实际的异步工作 - 我试图在这里确定的是简单地使它成为异步的开销
我不希望它变得更快只是因为我把它包装在异步中.我的问题恰恰相反:为什么戏剧性(恕我直言)放缓.
下面的评论正确地指出我应该为各种大小的数据集提供时间,这隐含地导致我在第一个实例中提出这个问题.
以下有时基于小型和大型数据集.虽然绝对值不太有意义,但相对性很有趣:
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倍.感到惊讶的是,随着数据集变大,时间差异没有摊销.如果是这种情况,那么流水线操作/并行化只能提高性能,如果你有两个以上的内核 - 超过我可以承受的开销解释......)
没有异步工作要做.实际上,你得到的只是管理费用而且没有任何好处.async {}并不意味着"大括号中的一切突然变得异步".它只是意味着你有一种使用异步代码的简化方法 - 但你永远不会调用一个异步函数!
另外,"异步"并不一定意味着"并行",并且它不一定涉及多个线程.例如,当你做一个异步请求读取文件(你不能在这里做什么),这意味着该操作系统被告知要做些什么,以及如何应通知,当它完成.当你使用这样的代码运行时RunSynchronously,你只是在发布异步文件请求时阻塞一个线程 - 这个场景与首先使用同步文件请求几乎完全相同.
在你做的那一刻RunSynchronously,你抛弃任何理由首先使用异步代码.你还在使用单个线程,你只是同时阻止另一个线程 - 而不是保存线程,你浪费一个,然后添加另一个线程来做真正的工作.
编辑:
好的,我用最小的例子进行了调查,我得到了一些观察.
async代码实际上没有异步执行任何操作,它也会创建大量异步构建器助手,Delay通过代码进行大量(异步)调用,并进入完全荒谬的区域,循环的每次迭代都是额外的方法调用,包括辅助对象的设置.显然,F#会自动转换while为异步while.现在,考虑到压缩xslt数据的通常情况,这些Read操作中涉及的I/O非常少,因此开销绝对占主导地位 - 并且由于"循环"的每次迭代都有自己的设置成本,因此开销会随着数据量的增加而变化.
虽然这主要是由于while实际上没有做任何事情,但显然也意味着你需要小心选择的内容async,并且你需要避免在CPU时间占主导地位的情况下使用它(比如在这种情况下 - 毕竟,异步和非异步情况在实践中几乎都是100%的CPU任务).由于Read一次读取一个节点这一事实进一步恶化- 即使在一个大的非压缩xml文件中也是相对微不足道的.开销绝对占主导地位.实际上,这类似于使用类似Parallel.For的身体sum += i- 每次迭代的设置成本绝对使任何实际工作相形见绌.
CPU分析使这一点变得相当明显 - 两个最耗费工作量的方法是:
XmlReader.Read (预期)Thread::intermediateThreadProc - 也称为"此代码在线程池线程上运行".像这样的无操作代码中的开销约为100% - yikes.显然,即使在任何地方都没有真正的异步性,回调也不会同步运行.循环帖子的每次迭代都会工作到新的线程池线程.吸取了教训?可能类似于" async如果循环体很少工作则不使用循环".循环的每次迭代都会产生开销.哎哟.