为什么我们没有通过PSeq获得性能提升?

时间:2018-03-27 23:35:37

标签: parallel-processing f# seq plinq

问题

我们认为并行处理每个“尾部置换”会减少该算法的实际运行时间。为什么不呢?如何配置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)

2 个答案:

答案 0 :(得分:4)

PSeq.fold没有按照您的想法行事。 PSeq.fold实际上根本不是平行的。这是顺序的。

你不能在算法中间的某处抛出“并行”这个词,并希望最好。这不是并行化的工作方式。你必须真正理解发生了什么,并行发生了什么,顺序发生了什么,原则上可以并行化什么,什么不可以。

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

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

  1. 递归计算尾部的排列。
  2. 对于每一个排列,通过在每个索引处插入头部来爆炸它。
  3. 连接第2步的所有结果。
  4. 当你这样说时,很容易看出第2步并不真正依赖于它本身。每个尾部排列都与其他尾部排列完全分开处理。

    当然,仅通过查看源代码就不明显了,因为您已将步骤2和3合并到一个表达式(insertAtEach xs.Head ps)@acc中,但使用以下一般标识可以轻松区分:< / p>

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

    也就是说,您可以在f期间将功能x应用于每个元素fold,而不是使用map将其“提前”应用于每个元素。< / p>

    将此想法应用于您的算法,您将得到:

    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)。

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

    P.P.S。 here's a gist with my benchmark code

答案 1 :(得分:2)

第一部分:“为什么不是?”

因为从 SEQ -- -- -- 流程执行计划到 PAR -- || || 的每次转换(转换)都需要付出一些努力才能完成确实明显具体化[TIME] - 域和[SPACE] - 域设置+终止费用

有关详细信息,请查看re-formulated Amdahl's Law, particularly the naive-form Fig.1部分:批评,然后推出限制性重新制定法律。

没有办法避免因支付税款和间接成本......

在并行计算中,在并发计算环境中并不那么密集,在交换中确实非常容易付款而不是接收

另外,人们可能会在StackOverflow上找到确实众多示例和惊喜,使用搜索标记,您可以看到许多带有基准开销成本的示例(如果用户仍然存在)思想开放,系统性足以确实能够运行并记录和发布现实的[TIME] - 域费用。

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

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

没人能。

然而,如果在初始产品规划期间使用,该工具还具有设计方面的优势。

净结果处理开销受资源池大小,强制数据传输,(非)避免共享和/或其他形式的相互依赖性影响,结果'大小返回{并发|并行} - 处理终止 - 所有这些附加成本都可以系统地进行基准测试,以便很好地理解这样做的成本,直到内存分配成本,垃圾收集或类似的现实运作方式解除分配费用。

鉴于已知的部署生态系统,在进入纯串行/分布式/并行流程执行之前,不允许任何人使用基准数据进行系统成本效益评估。

鉴于这种细粒度的知识是基准的,
它可以作为解决困境的公平指标
ie - PAR -- |||| 部分的最小尺寸是多少,以便它至少支付其自身不可避免的{ setup + termination } - 开销 - 附加费用?

没有完成其中任何一项,人们会感到惊讶,重新分解的过程有多昂贵,与直接“负面”或处理性能差于预期的方式形成鲜明对比改善。

始终:基准,基准,基准。

下一步:根本不要选择更昂贵的方式。