浮点操作在多个线程中运行时是否具有确定性



假设我有一个运行计算的函数,例如一个点积,我传入一个向量数组A, B和一个浮点数组C,函数分配:C[i] = dot(A[i], B[i]);

如果我创建并启动两个将运行此函数的线程,并将相同的三个数组传递给这两个线程,那么在什么情况下这种类型的操作(可能使用不同的非随机数学运算等)不能保证得到相同的结果(在同一台机器上运行相同的应用程序而不进行任何重新编译)?我只对消费者电脑的背景感兴趣。

我知道浮点运算通常是确定性的,但我确实想知道是否会发生一些奇怪的事情,也许在一个线程上计算会使用一个中间的80位寄存器,但在另一个线程中不会。

我认为,几乎可以保证相同的二进制代码在两个线程中运行(有没有办法不发生这种情况?由于某种原因,函数被编译了多次,编译器不知何故发现它将在多个线程中执行,并出于某种原因,为第二个线程再次编译它?)。但我有点担心CPU核心可能没有相同的指令集,即使是在消费者级别的PC上

附带问题-在类似的场景中,GPU怎么办?

//

我假设x86_64、Windows、c++和dota.x * b.x + a.y * b.y。无法提供更多信息-使用Unity IL2CPP,不知道它是如何编译/使用什么选项。

这个问题的动机:我正在写一个修改网格的计算几何程序——我称之为"网格";几何网格";。问题在于;渲染网格";对于某些几何位置有多个顶点-例如,平面着色需要它-您有多个具有不同法线的顶点。然而,实际的计算几何程序仅使用空间中位置的纯几何数据。

所以我看到两种选择:

  1. 创建从渲染网格到几何网格的映射(例如,将重复的顶点映射到一个唯一的顶点),在几何网格上运行该过程,然后根据结果以某种方式修改渲染网格
  2. 直接使用渲染网格。该过程对所有顶点进行计算时效率稍低,但从代码角度来看要容易得多。但最重要的是,我有点担心,对于两个位置相同的顶点,我可能会得到两个不同的值,而这不应该发生。仅使用位置,并且两个这样的顶点的位置将相同

浮点(FP)运算不是关联的(但它是交换的)。因此,(x+y)+z可以给出与x+(y+z)不同的结果。例如,对于64位IEEE-754浮点,(1e-13 + (1 - 1e-13)) == ((1e-13 + 1) - 1e-13)为false。C++标准对浮点数没有太大的限制。然而,广泛使用的IEEE-754标准是。它指定了32位和64位数字运算的精度,包括舍入模式。x86-64处理器符合IEEE-754标准,主流编译器(如GCC、Clang和MSVC)默认情况下也遵循IEEE-754。默认情况下,ICC是不兼容的,因为它假定FP操作是关联的,以提高性能。主流编译器有编译标志来做出这样的假设,从而加快代码的速度。它通常与其他假设相结合,比如假设所有FP值都不是NaN(例如-ffast-math)。这样的标志违反了IEEE-754标准,但它们经常用于3D或视频游戏行业,以加快代码的速度。C++标准不要求IEEE-754,但您可以使用std::numeric_limits<T>::is_iec559进行检查。

默认情况下,线程可以具有不同的舍入模式。但是,您可以使用此答案中提供的C代码设置舍入模式。此外,请注意,在某些平台上,非正规数字有时会被禁用,因为它们的开销非常高(有关更多信息,请参阅此部分)。

假设IEEE-754合规性没有被破坏,舍入模式是相同的,线程以相同的顺序进行操作,那么结果应该是相同的——至少达到1 ULP。在实践中,如果使用相同的主流编译器编译它们,结果应该完全相同。

问题是使用多个线程通常会导致应用的FP操作的顺序不确定,从而导致不确定的结果。更具体地说,FP变量上的原子操作通常会导致这样的问题,因为操作的顺序经常在运行时发生变化。如果想要确定性结果,则需要使用静态分区,避免对FP变量进行原子操作,或者更常见的是,避免可能导致不同排序的原子操作。同样的事情也适用于锁或任何同步机制。

GPU也是如此。事实上,当开发人员使用原子FP操作对值求和时,这种问题非常常见。他们经常这样做是因为实现快速缩减很复杂(尽管它更具确定性),而且原子操作在现代GPU上也很快(因为他们使用专用的高效单元)。

根据浮点处理器非确定性的公认答案?,C++浮点不是非确定性的。相同的指令序列将给出相同的结果。

不过,有几件事需要考虑:

首先,特定C++源代码执行FP计算的行为(即结果)可能取决于编译器和所选的编译器选项。例如,它可能取决于编译器是选择发出64位FP指令还是80位FP指令。但这是决定性的。

其次,相似的C++源代码可能会给出不同的结果;例如由于某些FP指令的非关联行为。这也是确定性的。

默认情况下,确定性不会受到多线程的影响。C++编译器可能不知道代码是否是多线程的。而且它绝对没有理由发出不同的FP代码。

诚然,FP行为取决于所选的舍入模式,并且可以在每个线程的基础上进行设置。然而,要实现这一点,必须为不同的线程显式地设置不同的舍入模式(应用程序代码)。再一次,这是决定性的。(对于应用程序代码来说,这是一件非常愚蠢的事情。)


一台电脑会使用不同的FP硬件,对不同的线程有不同的行为,这种想法对我来说似乎很牵强。当然,一台电脑可能有(比如)英特尔芯片组和ARM芯片组,但同一C++应用程序(可执行文件)的不同线程同时在两个芯片组上运行是不可能的。

GPU也是如此。事实上,考虑到您需要以与普通(或线程化)C++完全不同的方式对GPU进行编程,我怀疑它们是否可以共享相同的源代码。


简而言之,我认为你正在担心一个你在现实中不太可能遇到的假设性问题。。。考虑到硬件和C++编译器的当前技术状态。

相关内容

  • 没有找到相关文章

最新更新