在多线程中 运行 时,浮点运算是否具有确定性?

Are floating point operations deterministic when running in multiple threads?

假设我有一个 运行s 计算的函数,示例类似于点积 - 我传入一个向量数组 A, B 和一个浮点数组 C,并且功能分配: C[i] = dot(A[i], B[i]);

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

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

我认为几乎可以保证在两个线程中 运行 相同的二进制代码(有什么方法不会发生这种情况吗?由于某种原因该函数被多次编译,编译器不知何故弄清楚了它会 运行 在多个线程中,并出于某种原因为第二个线程再次编译它?)。 但我更担心 CPU 内核可能没有相同的指令集,即使在消费级 PC 上也是如此。

附带问题 - 在类似情况下 GPU 怎么样?

//

我假设 x86_64、Windows、c++ 和 dota.x * b.x + a.y * b.y。无法提供更多信息 - 使用 Unity IL2CPP,不知道如何 compiles/with 有哪些选项。

提问的动机:我正在编写一个修改网格的计算几何程序 - 我将其称为“几何网格”。问题是“渲染网格”可能会在某些几何位置上有多个顶点——例如,平面着色需要它——你有多个具有不同法线的顶点。然而,实际的计算几何程序仅使用 space.

中位置的纯几何数据

所以我看到两个选项:

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

根据 floating point processor non-determinism? 的公认答案,C++ 浮点不是 non-deterministic。相同的指令序列将给出相同的结果。

不过有几点需要考虑:

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

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

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

诚然,FP 行为取决于所选的舍入模式,并且可以在 per-thread 基础上进行设置。然而,要做到这一点,某些东西(应用程序代码)必须为不同的线程显式设置不同的舍入模式。再一次,这是确定性的。 (对于应用程序代码来说,这是一件非常愚蠢的事情,IMO。)


PC 会使用不同的 FP 硬件,对不同的线程具有不同的行为的想法 far-fetched 在我看来。当然,一台 PC 可能有(比如说)Intel 芯片组和 ARM 芯片组,但同一 C++ 应用程序(可执行文件)的不同线程在两个芯片组上同时 运行 是不合理的。

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


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

浮点 (FP) 运算不是关联的(但它是可交换的)。因此,(x+y)+z 可以给出与 x+(y+z) 不同的结果。例如,(1e-13 + (1 - 1e-13)) == ((1e-13 + 1) - 1e-13) 对于 64 位 IEEE-754 浮点数是假的。 C++ 标准对 floating-point 数字的限制不是很大。但是,widely-used 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.

进行检查

默认情况下,线程可以有不同的舍入模式。但是,您可以使用 this answer. Also, please note that denormal numbers are sometimes disabled on some platforms because of their very-high overhead (see this 中提供的 C 代码来设置舍入模式以获取更多信息。

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

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

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