为什么 std::reduce 需要交换性?

Why does std::reduce need commutativity?

本文关键字:交换 std reduce 为什么      更新时间:2023-10-16

https://en.cppreference.com/w/cpp/algorithm/reduce

它说如果操作不是可交换的,则不定义操作的行为,但为什么呢?我们只是将数组划分为块,然后合并结果。是否只需要具有关联性?

std::reduce需要结合性和交换性。并行算法显然需要关联性,因为您希望在单独的块上执行计算,然后将它们组合在一起。

至于交换性:根据MSVC STL开发人员Billy O'Neal的reddit帖子,这是允许矢量化到SIMD指令所必需的:

交换性对于启用矢量化也是必要的,因为您希望reduce的代码如下所示:

vecRegister = load_contiguous(first);
while (a vector register sized chunk is left) {
first += packSize;
vecRegister = add_packed(load_contiguous(first), vecRegister);
}
// combine vecRegister's packed components

等等,其中给定整数和 SSE 寄存器和

a * b * c * d * e * f * g * h 给出类似 (a * e( * (b * f( * (c * g( * (d * h( 的东西。大多数其他语言并没有做明确的事情来使矢量化它们的减少成为可能。没有什么说如果有人提出一个引人注目的用例,我们将来不能添加一个noncommutative_reduce或类似的东西。

如果操作数之间的操作不是可交换的,则行为实际上是不确定的。"非确定性"与"未定义"不同。例如,浮点数学不是可交换的。这就是为什么对std::reduce的调用可能不是确定性的,因为二进制函数以未指定的顺序应用。

请参阅标准中的此注释:

注意:reduceaccumulate之间的区别在于,binary_op在 未指定顺序,它为非关联或非交换产生非确定性结果 binary_op,例如浮点加法。—尾注 ]

该标准将广义和定义如下: numeric.defns

定义GENERALIZED_NONCOMMUTATIVE_SUM(op, a1, ..., aN(,如下所示:

  • 当 N 为 1 时为 a1,否则

  • op(GENERALIZED_NONCOMMUTATIVE_SUM(op, a1, ..., aK(, op(GENERALIZED_NONCOMMUTATIVE_SUM(op, aM, ..., aN(( 对于任何 K 其中 1

将GENERALIZED_SUM(op, a1, ..., aN( 定义为 GENERALIZED_NONCOMMUTATIVE_SUM(op, b1, ..., bN(,其中 b1, ...,bN 可以是 a1, ..., aN 的任何排列。

因此,求和的顺序以及操作数的顺序是未指定的。因此,如果二进制运算不是可交换的或非关联的,则结果是未指定的。

这里也明确指出了这一点。

关于原因:它给了图书馆供应商更多的自由,所以他们可能会也可能不会更好地实施它。作为实现可以从交换性中受益的示例。考虑a+b+c+d+e和,我们首先并行计算a+bc+d。现在a+bc+d之前返回(因为它可能发生,因为它是并行完成的(。我们现在可以直接计算(a+b)+e,然后将此结果添加到c+d的结果中,而不是等待c+d的返回值。所以最后我们计算了((a+b)+e)+(c+d),这是a+b+c+d+e的重排。

为什么std::reduce需要交换性?

为了速度。

如果运算符是可交换的,则可以重新排列运算顺序而不影响结果。

如果你能重新排列操作的顺序,你可以让不同的线程,或者进程,或者硬件加速器或者什么的——对一些要执行的操作独立工作,而不关心它们完成部分总和的顺序,也不关心它们内部的操作顺序,然后最后以任何方便的方式将部分总和相加。