Raku parallel/functional 方法

Raku parallel/functional methods

我对 Raku 还很陌生,我对函数式方法有疑问,尤其是 reduce。 我原来的方法是:

sub standardab{
  my $mittel = mittel(@_);
  my $foo = 0;
  for @_ {
    $foo += ($_ - $mittel)**2;
  }
  $foo = sqrt($foo/(@_.elems));
}

而且效果很好。然后我开始使用 reduce:

sub standardab{
    my $mittel = mittel(@_);
    my $foo = 0;
    $foo = @_.reduce({$^a + ($^b-$mittel)**2});
    $foo = sqrt($foo/(@_.elems));
}

我的执行时间翻了一番(我将其应用于大约 1000 个元素)并且解决方案相差 0.004(我猜是舍入误差)。 如果我使用

.race.reduce(...)

我的执行时间是原始顺序代码的 4 倍。 有人可以告诉我这是为什么吗? 我考虑过并行初始化时间,但是 - 正如我所说 - 我将其应用于 1000 个元素,如果我更改代码中的其他 for 循环以减少它,它会变得更慢!

感谢您的帮助

总结

  • 通常,reducefor 做不同的事情,并且它们在您的代码中做不同的事情。例如,与您的 for 代码相比,您的 reduce 代码涉及传递的参数数量是原来的两倍,并且迭代次数减少了一次。我认为这可能是 0.004 差异的根源。

  • 即使您的 forreduce 代码执行相同的操作,此类 reduce 代码的优化版本也永远不会比同样优化的版本更快等效的 for 代码。

  • 由于 reduce 的性质,我认为 race 不会自动并行化 reduce。 (虽然我看到你和@user0721090601 的评论我错了。)但是它 产生开销——目前 很多.

  • 可以 使用 race 来并行化你的 for 循环,如果它被稍微重写的话。 可能加快速度。

关于您的 forreduce 代码之间的区别

这是我的意思:

say do for    <a b c d>  { $^a }       # (a b c d)      (4 iterations)

say do reduce <a b c d>: { $^a, $^b }  # (((a b) c) d)  (3 iterations)

有关它们操作的更多详细信息,请参阅它们各自的文档 (for, reduce)。

您还没有共享您的数据,但我假设 for and/or reduce 计算涉及 Nums(浮点数)。浮点数的加法是不可交换的,所以如果加法最终以不同的顺序发生,你可能会得到(通常很小的)差异。

我想这解释了 0.004 的区别。

你的顺序 reduce 比你的 for

慢 2 倍

my execution time doubled (I am applying this to roughly 1000 elements)

首先,如上所述,您的 reduce 代码不同。存在一般的抽象差异(例如,每次调用采用两个参数而不是 for 块的一个),并且您的特定数据可能导致基本的数字计算差异(也许您的 for 循环计算主要是整数或浮点数学而你的 reduce 主要是理性的?)。 可能 解释了执行时间的差异,或者其中的一部分。

一方面,它的另一部分可能是 reduce 之间的区别,后者默认编译为闭包调用,具有调用开销,每个调用有两个参数,并且临时内存存储中间结果,另一方面,for 默认编译为直接迭代,{...} 只是内联代码而不是闭包调用。 (也就是说,reduce 有时可能会编译为内联代码;您的代码甚至可能已经是这样了。)

更一般地说,Rakudo 优化工作仍处于相对早期的阶段。其中大部分是通用的,可以加速所有代码。在对特定结构进行了努力的地方,到目前为止,最广泛使用的结构得到了关注,for 被广泛使用,而 reduce 则较少。所以部分或全部差异可能只是 reduce 优化不佳。

reducerace

my execution time [for .race.reduce(...)] is 4 times higher than with the original sequential code

我认为 reduce 不会 自动 race 并行化。根据 its docreduce 由 "iteratively applying a function which knows how to combine two values" 工作,每次迭代中的一个参数是前一次迭代的结果。所以在我看来它必须按顺序完成。

(我在评论中看到我误解了编译器可以通过归约完成什么。也许这是交换操作?)

总而言之,您的代码产生了 raceing 的开销而没有获得任何好处。

一般race

假设您正在使用 可与 race.

并行的某些操作

首先,如您所述,race 会产生开销。将有初始化和拆卸成本,至少其中一些成本是针对整体 statement/expression 的每次评估重复支付的,即 raced.

其次,至少目前,race 意味着在 CPU 核心上使用线程 运行ning。对于某些有效载荷,尽管有任何初始化和拆卸成本,但仍能产生有用的好处。但它充其量只是与内核数量相等的加速。

(编译器实现者总有一天会发现 raced for 循环足够简单,可以在 GPU 上实现 运行 而不是 CPU,然后将其发送到 GPU 以实现惊人的加速。)

第三,如果你真的写 .race.foo... 你会得到 默认 赛车的一些可调方面的设置。默认值几乎肯定不是最佳的,可能会偏离。

当前可调设置为 :batch:degree。有关详细信息,请参阅 their doc

更一般地说,并行化是否加速代码取决于特定用例的细节,例如使用的数据和硬件。

关于将 racefor

一起使用

如果您稍微重写一下代码,您可以 race 您的 for:

$foo = sum do race for @_ { ($_ - $mittel)**2 } 

要应用调整,您必须重复 race 作为一种方法,例如:

$foo = sum do race for @_.race(:degree(8)) { ($_ - $mittel)**2 }