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