【问题标题】:Recursive sync faster than Recursive async递归同步比递归异步更快
【发布时间】:2011-08-16 21:17:32
【问题描述】:

为什么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 会更快,因为它是异步的,并且我以并行方式运行它。我错过了什么?

【问题讨论】:

  • 您要处理多少个文件夹? async 有一些开销,在某些情况下,可能会抵消好处。
  • 对于这个特殊的问题,你会创建很多短命的asyncs。也就是说,成本/收益比特别高。对于树形结构,最好使用固定数量的异步遍历器。
  • 我认为在这种情况下,您几乎不进行任何受 CPU 限制的计算,而只遍历受 IO 限制的文件系统,您不会从运行中获得任何加速并行,只是开销。
  • AsyncGetDirectories 是从哪里来的?

标签: f#


【解决方案1】:

正如 Daniel 和 Brian 已经清楚解释的那样,您的解决方案可能会创建太多短暂的异步计算(因此开销大于并行性的收益)。 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 之间的任何嵌套结果都相似)

【讨论】:

    【解决方案2】:

    来自cmets:

    您的函数会产生 async 的成本而没有任何好处,因为

    1. 您创建了太多的asyncs 来完成短时间的工作
    2. 你的函数不是 CPU,而是 IO,绑定

    【讨论】:

    • IO-bound在这里还是很有用的;交错 IO 比请求文件、等待、获取文件、请求另一个文件、等待、获取另一个文件同步/串行要好得多。
    【解决方案3】:

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

    关键是负载平衡和粒度。工作量太小,异步的开销超过了并行的好处。因此,您需要足够大的工作块来利用并行并克服开销。但是如果工作量太大且不平衡(例如,一个*目录有 10000 个文件,而其他 3 个*目录每个有 1000 个文件),那么您也会受到影响,因为一个人很忙,而其他人很快完成,而您不要最大化并行度。

    如果您可以预先估计每个子树的工作,您可以进行更好的调度。

    【讨论】:

      【解决方案4】:

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

      【讨论】: