递归同步比递归异步更快

时间:2011-08-16 21:17:33

标签: f#

为什么Solution 2Solution 1效率更高?

(时间是100次运行的平均值,它们经过的总文件夹数是13217)

// Solution 1 (2608,9ms)
let rec folderCollector path =
  async { let! dirs = Directory.AsyncGetDirectories path 
          do! [for z in dirs -> folderCollector z] 
              |> Async.Parallel |> Async.Ignore }

// Solution 2 (2510,9ms)
let rec folderCollector path =
  let dirs = Directory.GetDirectories path 
  for z in dirs do folderCollector z

我原以为Solution 1会更快,因为它是异步的,我在并行运行它。我缺少什么?

4 个答案:

答案 0 :(得分:6)

正如丹尼尔和布莱恩已经清楚解释的那样,你的解决方案可能是创造了太多短暂的异步计算(因此开销不仅仅来自并行性的收益)。 AsyncGetDirectories操作也可能不是非阻塞的,因为它没有做太多工作。我没有在任何地方看到这个操作的真正异步版本 - 它是如何定义的?

无论如何,使用普通的GetDirectories,我尝试了以下版本(只创建了少量的并行异步):

// Synchronous version
let rec folderCollectorSync path =
    let dirs = Directory.GetDirectories path 
    for z in dirs do folderCollectorSync z

// Asynchronous version that uses synchronous when 'nesting <= 0'
let rec folderCollector path nesting =
    async { if nesting <= 0 then return folderCollectorSync path 
            else let dirs = Directory.GetDirectories path 
                 do! [for z in dirs -> folderCollector z (nesting - 1) ] 
                     |> Async.Parallel |> Async.Ignore }

在一定数量的递归调用之后调用一个简单的同步版本是一个常见的技巧 - 在并行化任何非常深的树状结构时使用它。使用folderCollector path 2,这将只启动数十个并行任务(而不是数千个),因此效率会更高。

在我使用的示例目录(包含4800个子目录和27000个文件)上,我得到:

  • folderCollectorSync path需要 1秒
  • folderCollector path 2需要 600ms (对于1到4之间的任何嵌套,结果类似)

答案 1 :(得分:3)

来自评论:

您的功能会产生async的费用而没有任何好处,因为

  1. 为了完成短期工作,你创建了太多async
  2. 你的功能不是CPU,而是IO,绑定

答案 2 :(得分:1)

我希望遇到这样的问题,如果在顶层执行异步/并行工作,您可能会得到最好的结果,但是子调用会同步。 (或者如果树很深,也许前两个级别是异步的,然后在那之后同步。)

密钥是负载平衡和粒度。太小的工作,异步的开销超过了并行的好处。因此,您需要足够大的工作来利用并行并克服开销。但是如果工件太大而且不平衡(例如,一个顶级目录有10000个文件,另外3个顶级目录有1000个),那么你也会受苦,因为一个人很忙而其余人很快完成,而你不要最大化并行性。

如果您可以预先估算每个子树的工作,您可以做更好的安排。

答案 3 :(得分:0)

显然,您的代码是IO绑定的。请记住硬盘驱动器的工作原理。当您使用Async进行多次读取时,HDD的读取头必须来回跳转以同时提供不同的读取命令,这会引入延迟。如果磁盘上的数据严重分散,这可能会变得更糟。