【问题标题】:Why do we not get a performance improvement with PSeq?为什么我们没有通过 PSeq 获得性能提升?
【发布时间】:2018-03-27 23:35:36
【问题描述】:

问题

我们认为并行处理每个“尾排列”会减少该算法的实际运行时间。为什么不呢?如果我们可以配置 PSeq 以提高运行时间,该怎么办?

let rec permute (xs:'t list) = 
    if xs.Length = 1 then [xs]
    else ([], permute xs.Tail)
        // Why do we not get a performance 
        // improvement when we use PSeq here
        // relative to using Seq?
        ||> PSeq.fold (fun acc ps -> 
            (insertAtEach xs.Head ps)@acc)

permute ["A";"B";"C"] |> ignore

我们认为排列工作将按如下方式拆分,并且算法的insertAtEach 阶段将并行工作。

                        [C]

             [BC]                [CB]

[ABC] [BCA] [BCA]                [ACB] [CAB] [CBA]

            CPU01                CPU02

即使我们使用较大的初始列表(例如permute [0..9]),它实际上也更慢。我们还没有弄清楚withDegreeOfParallelism 和相关的 PSeq 选项是否会有所帮助。

一边

下面是其余的代码清单:

let put a q xs =
    ([], xs)
        ||> Seq.fold (fun acc x ->
            if x = q then a::x::acc
            else x::acc)
        |> List.rev

// Insert x at each possible location in xs
let insertAtEach a xs =
    ([a::xs], xs)
        ||> Seq.fold (fun acc x ->
            (put a x xs)::acc)

【问题讨论】:

  • 据我了解,F# 并行代码的固定成本相对较高,即使使用permute [0..9] 也可能不足以克服固定成本。我建议首先进行多次测量:比较 8 个元素排列的串行与并行运行时间,然后是 9,然后是 10,然后是 11,然后是 12……并绘制时间图。看起来并行版本会一直领先于串行版本吗?那么并行版本效率低下可能有一些原因。但如果看起来并行版本最终会赶上来,那么它只是固定成本。
  • @rmunn 我需要用不同的方法来尝试,因为在现有的实现中,permute [0..10] 使用 Seq 或 PSeq 方法花费的时间太长(超过 15 分钟)尽管permute [0..9] 只需要 15 秒左右。
  • @rmunn 虽然您所说的通常是正确的,但在这种特殊情况下,问题是 fold 原则上不可并行化。 :-)

标签: parallel-processing f# seq plinq


【解决方案1】:

PSeq.fold 并没有按照你的想法去做。 PSeq.fold 实际上根本不平行。它是连续的。


您不能只是在算法中间的某个地方加上“并行”一词,然后寄希望于最好的结果。这不是并行化的工作方式。您必须真正了解正在发生的事情、并行发生的事情、顺序的事情、原则上可以并行化的事情以及不能并行化的事情。

fold 为例:它将提供的折叠函数应用于序列的每个元素上一次调用的结果。由于每次下一个调用都必须有上一个调用的结果才能执行,所以很明显fold 根本不能并行执行。必须是顺序的。事实上,如果您查看它的source code,这就是PSeq.fold 的实际作用。因此,每次转换为 ParallelSequence 都会产生一些开销,但没有任何收获。

现在,如果您仔细查看您的算法,您可以梳理出可并行化的部分。你的算法是做什么的:

  1. 递归计算尾部的排列。
  2. 对于其中的每一个排列,通过在每个索引处插入头部来分解它。
  3. 连接第 2 步的所有结果。

当您这样说时,很容易看出第 2 步实际上并不依赖于任何东西,而是依赖于自身。每个尾排列都与其他所有排列完全分开处理。

当然,这并不能仅仅从您的源代码中看出,因为您已将第 2 步和第 3 步组合在一个表达式中 (insertAtEach xs.Head ps)@acc,但使用以下通用标识很容易区分:

xs |> fold (fun a x -> g a (f x))
    ===
xs |> map f |> fold (fun a x -> g a x)

也就是说,您可以使用map“提前”将函数f 应用于每个元素x,而不是在x 上应用。

将这个想法应用到你的算法中,你会得到:

let rec permute (xs:'t list) = 
    if xs.Length = 1 then [xs]
    else permute xs.Tail 
         |> Seq.map (insertAtEach xs.Head)
         |> Seq.fold (fun acc ps -> ps@acc) []

现在很容易看出Seq.map 步骤是可并行化的:map 将函数独立于其他元素应用到每个元素,因此它可以 原则上并行工作。只需将 Seq 替换为 PSeq 即可获得并行版本:

let rec permute (xs:'t list) = 
    if xs.Length = 1 then [xs]
    else permute xs.Tail 
         |> PSeq.map (insertAtEach xs.Head)
         |> PSeq.fold (fun acc ps -> ps@acc) []

PSeq 是否确实并行执行map 是另一回事,但这应该很容易通过经验验证。

事实上,在我的机器上,并行版本在 7 到 10 个元素的列表中始终优于顺序版本(11 元素列表导致 OOM)。


附:请记住,在测量时间时,您需要先强制生成序列(例如,通过将其转换为列表或采用Seq.last)。否则,您所测量的只是并行化开销成本。

附言here's a gist with my benchmark code.

【讨论】:

  • 您提到“PSeq.fold 实际上根本不是并行的。它是顺序的。”根据源代码,你是对的。奇怪的是,它的documentation 另有说明:“使用 System.Linq.Parallel 并行运行。”甚至corefx documentation 也表示它是并行运行的。你知道文档是什么意思吗?
  • 这可能是因为文档是由某种模板生成的,没有人有意识去细化它。文档不能代替您自己的想法。您必须始终考虑自己在做什么。这就是软件工程师薪酬如此之高的原因。
  • 理论上,如果您知道折叠操作是关联的,您可以并行化该操作。例如,如果你在做 Array.fold (+)(让我们暂时假设 Array.sum 不存在),那么因为等式 (a + b) + c = a + (b + c) 将始终适用于所有值在 a、b 和 c 中,您可以安全地将数组的前两个元素添加到 CPU 1 上,将第三个和第四个元素添加到 CPU 2 上,依此类推。但由于您不会总是折叠关联操作,因此 PSeq.fold 不能假设任何内容,因此它必须连续执行每个步骤。
  • 回复:误导性文档,我在 corefx 存储库中打开了 an issue
  • @rmunn 这对我来说很有意义,并提醒我我们可以根据聚合操作实现大多数(如果不是全部)LINQ 操作。
【解决方案2】:

第一部分:“为什么没有?”

因为从 SEQ -- -- -- 进程执行调度到 PAR -- || || 的每次转换(转换)都需要付出一些努力,这表明它们确实是可见的物化了[TIME]-domain 和 [SPACE]-domain 设置 + 终止间接费用

如需了解更多详情,请查看 re-formulated Amdahl's Law, particularly the naive-form Fig.1部分:批评 以及后来衍生的开销严格的法律重新制定。

没有办法避免支付税款和间接费用......

在并行计算中,在并行计算环境中的密集程度稍低一些,确实支付比接收更容易作为交换。

另外,您可能会发现确实 大量示例 并在 StackOverflow 上使用搜索标签 在这里发布了惊喜,您可以看到许多具有基准管理费用的示例(如果用户保持开放-有思想并且足够系统地运行它们并记录和发布现实的[TIME]-domain 成本)。


第二部分:“如何如果我们可以……改善运行时间?”

没有人可以摆脱开销严格、原子处理意识重新制定的阿姆达尔定律。

没有人可以。

不过,如果在初始产品规划期间使用此工具,它还具有设计方面的优势。

产生的净处理开销受资源池大小、强制数据传输、(未)避免的共享和/或其他形式的相互依赖、结果大小在{并发@时返回的影响。 987654329@parallel }-processing 终止 -- 所有这些附加成本都可以系统地进行基准测试,以便更好地了解这样做的成本,包括内存分配成本、垃圾收集或类似的现实作案手法释放开销。

鉴于一个已知的部署生态系统,没有人被禁止使用基准数据进行系统的成本效益评估,以进入纯串行/分布式/并行流程执行。

鉴于这种细粒度的知识已经过基准测试,
它可以作为解决困境的公平指标
即 -- PAR -- |||| 部分的最小尺寸是多少,这样它至少可以支付自己不可避免的{ setup + termination }-overhead-add-on-costs?

没有做这两个,你会感到惊讶,与直接“负面”或比预期更差的处理性能相比,重构过程的成本是多么高改进。

始终:基准、基准、基准。

下一步:永远不要选择更昂贵的方式。

【讨论】:

  • 嗯...什么?这里有一些有用的信息,但是你的写作风格让你很难理解你在说什么。你能试着用更简单的语言解释一下吗?或者至少解释一下SEQ -- -- --PAR -- || || 之类的速记是什么意思?没有这么多不透明的速记的简化解释将大大有助于使这个答案易于理解。
猜你喜欢
  • 1970-01-01
  • 2013-08-12
  • 2011-08-10
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-05-24
相关资源
最近更新 更多