浮点计算在处理器上既不关联也不分布。所以,
(a + b) + c
不等于a + (b + c)
并且a * (b + c)
不等于a * b + a * c
有没有办法执行不给出不同结果的确定性浮点计算。当然,这在单处理器上是确定性的,但在多线程程序中,如果线程相加,例如,它就不是确定性的,因为线程可能存在不同的交错。
所以我的问题是,如何在多线程程序中实现浮点计算的确定性结果?
浮点计算在处理器上既不关联也不分布。所以,
(a + b) + c
不等于a + (b + c)
并且a * (b + c)
不等于a * b + a * c
有没有办法执行不给出不同结果的确定性浮点计算。当然,这在单处理器上是确定性的,但在多线程程序中,如果线程相加,例如,它就不是确定性的,因为线程可能存在不同的交错。
所以我的问题是,如何在多线程程序中实现浮点计算的确定性结果?
浮点是确定性的。相同的浮点运算,在相同的硬件上运行,总是产生相同的结果。没有黑魔法、噪音、随机性、模糊或人们通常归因于浮点的任何其他东西。牙仙没有出现,拿走你的结果的低位,在你的枕头下留下四分之一。
现在,也就是说,某些通常用于大规模并行计算的阻塞算法在执行浮点计算的顺序方面是不确定的,这可能导致跨运行的结果不精确。
你能为这个做什么?
首先,确保你真的无法忍受这种情况。您可能尝试在并行计算中强制执行排序的许多事情都会损害性能。就是这样。
我还要注意,尽管阻塞算法可能会引入一些不确定性,但它们通常会提供比天真的未阻塞串行算法更小的舍入误差的结果(令人惊讶但确实如此!)。如果你能忍受朴素的串行算法产生的错误,你可能也能忍受并行阻塞算法的错误。
现在,如果您真的,真的,需要跨运行的精确再现性,这里有一些不会对性能产生太大负面影响的建议:
不要使用可以重新排序浮点计算的多线程算法。问题解决了。这并不意味着您根本不能使用多线程算法,只是您需要确保每个单独的结果仅由同步点之间的单个线程接触。请注意,如果处理得当,这实际上可以通过减少内核之间的 D$ 争用来提高某些架构的性能。
在归约操作中,您可以让每个线程将其结果存储到数组中的索引位置,等待所有线程完成,然后按顺序累积数组的元素。这会增加少量的内存开销,但通常是可以容忍的,尤其是当线程数“很少”时。
想办法提升并行度。不是计算 24 个矩阵乘法,每个乘法使用并行算法,而是并行计算 24 个矩阵乘积,每个乘法使用串行算法。这也可能对性能有益(有时非常有益)。
还有很多其他方法可以处理这个问题。他们都需要思考和关心。并行编程通常可以。
编辑:我已经删除了我的旧答案,因为我似乎误解了 OP 的问题。如果您想查看它,您可以阅读编辑历史记录。
我认为理想的解决方案是切换到每个线程都有一个单独的累加器。这避免了所有锁定,这会对性能产生巨大影响。您可以在整个操作结束时简单地对累加器求和。
或者,如果您坚持使用单个累加器,一种解决方案是使用“定点”而不是浮点。这可以通过在您的累加器中包含一个巨大的“偏差”项以将指数锁定在一个固定值来使用浮点类型来完成。例如,如果您知道累加器永远不会超过 2^32,您可以在0x1p32
. 这会将您锁定在小数点左侧的 32 位精度和 20 位小数精度(假设double
)。如果精度不够,您可以使用较小的偏差(假设累加器不会变得太大)或切换到long double
. 如果long double
是 80 位扩展格式,则 2^32 的偏差将给出 31 位的小数精度。
然后,每当您想实际“使用”累加器的值时,只需减去偏置项即可。
即使使用高精度定点数据类型也不能解决使所述方程的结果具有确定性的问题(某些情况除外)。正如 Keith Thompson 在评论中指出的那样,1/3 是一个无法以标准 base-10 或 base-2 浮点表示形式正确存储的值的反例(无论使用的精度或内存如何)。
根据特定需要,可以解决此问题(它仍然有限制)的一种解决方案是使用有理数数据类型(一种同时存储分子和分母的数据类型)。Keith 建议将GMP作为这样的库之一:
GMP 是用于任意精度算术的免费库,可对有符号整数、有理数和浮点数进行操作。精度没有实际限制...
它是否适合(或足够)这项任务是另一回事......
快乐编码。
使用十进制类型或支持这种类型的库。
尝试将每个中间结果存储在 volatile 对象中:
volatile double a_plus_b = a + b;
volatile double a_plus_b_plus_c = a_plus_b + c;
这可能会对性能产生不良影响。我建议测量两个版本。
编辑: 的目的volatile
是禁止即使在单线程环境中也可能影响结果的优化,例如更改操作顺序或将中间结果存储在更宽的寄存器中。它不解决多线程问题。
EDIT2:还有一点需要考虑的是
浮动表达式可以被压缩,也就是说,像原子操作一样进行评估,从而忽略源代码和表达式评估方法隐含的舍入错误。
这可以通过使用来抑制
#include <math.h>
...
#pragma STDC FP_CONTRACT off
参考:C99 标准(大 PDF),第 7.12.2 节和第 6.5 节第 8 段。这是 C99 特定的;一些编译器可能不支持它。
使用压缩十进制。