浮点计算既不是关联的,也不是处理器上的分配。所以,
(a + b) + c
不等于a + (b + c)
且a * (b + c)
不等于a * b + a * c
有没有办法执行不会给出不同结果的确定性浮点计算。它在单处理器上是确定性的,但是如果线程增加了一个总和,那么它在多线程程序中就不是确定性的,因为线程可能有不同的交错。
所以我的问题是,如何在多线程程序中实现浮点计算的确定性结果?
答案 0 :(得分:15)
浮点是确定性的。在相同硬件上运行的相同浮点运算始终产生相同的结果。没有黑魔法,噪音,随机性,模糊测试或人们通常归因于浮点的任何其他事物。牙仙没有出现,取你的结果的低位,并在你的枕头下留下四分之一。
现在,那就是说,通常用于大规模并行计算的某些阻塞算法在执行浮点计算的顺序方面是非确定性的,这可能导致跨运行的非精确结果。
你能做些什么?
首先,确保你实际上不能忍受这种情况。您可能尝试在并行计算中强制执行排序的许多事情都会损害性能。就是这样。
我还要注意,尽管被阻塞的算法可能会引入一定数量的非确定性,但它们经常会提供较小的舍入错误的结果,而不是天真的未阻塞的串行算法(令人惊讶但却是真的!)。如果你能忍受一个天真的串行算法产生的错误,你可能会遇到并行阻塞算法的错误。
现在,如果您真的,确实需要跨运行的完全重现性,这里有一些建议往往不会对性能产生太大影响:
不要使用可以重新排序浮点计算的多线程算法。问题解决了。这并不意味着您根本不能使用多线程算法,只需要确保每个单独的结果仅由同步点之间的单个线程触及。请注意,如果正确完成,通过减少核心之间的D $争用,这实际上可以提高在某些体系结构上的性能。
在缩减操作中,您可以让每个线程将其结果存储到数组中的索引位置,等待所有线程完成,按顺序累积数组的元素。这会增加少量的内存开销,但通常是可以忍受的,特别是当线程数量很小时。
找到提升并行性的方法。而不是计算24个矩阵乘法,每个矩阵乘法使用并行算法,并行计算24个矩阵乘积,每个矩阵乘积使用串行算法。这也有利于表现(有时非常有用)。
还有很多其他方法可以解决这个问题。他们都需要思考和关心。并行编程通常会这样做。
答案 1 :(得分:3)
编辑:我删除了旧答案,因为我似乎误解了OP的问题。如果你想看到它,你可以阅读编辑历史。
我认为理想的解决方案是切换到每个线程都有一个单独的累加器。这避免了所有锁定,这应该对性能产生巨大影响。您可以在整个操作结束时简单地对累加器求和。
或者,如果你坚持使用单个累加器,一种解决方案是使用“定点”而不是浮点。这可以通过在累加器中包含一个巨大的“偏置”项来将浮点类型锁定,以将指数锁定在固定值。例如,如果您知道累加器永远不会超过2 ^ 32,则可以在0x1p32
处启动累加器。这将锁定小数点左边的32位精度和20位小数精度(假设为double
)。如果这不够精确,你可以给我们一个较小的偏差(假设累加器不会变得太大)或切换到long double
。如果long double
是80位扩展格式,则偏差为2 ^ 32将得到31位小数精度。
然后,只要你想实际“使用”累加器的值,只需减去偏差项。
答案 2 :(得分:2)
即使使用高精度定点数据类型也无法解决使所述方程的结果具有确定性的问题(在某些情况下除外)。正如Keith Tomposon在评论中指出的那样,1/3是一个无关紧要的例子,它无法在标准的base-10或base-2浮点表示中正确存储(无论使用的精度或内存如何)。 / p>
根据特定需要,可以解决此问题(它仍然有限制)的一种解决方案是使用Rational number数据类型(存储分子和分母的数据类型)。基思建议将GMP作为一个这样的库:
GMP是一个免费的库,用于任意精度算术,对有符号整数,有理数和浮点数进行操作。精度没有实际限制......
这项任务是否合适(或足够)是另一个故事......
快乐的编码。
答案 3 :(得分:1)
使用支持此类型的小数类型或库。
答案 4 :(得分:-2)
尝试将每个中间结果存储在易失性对象中:
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 standard(大型PDF),第7.12.2和6.5段第8段。这是C99特有的;一些编译器可能不支持它。
答案 5 :(得分:-4)