是否可以清除 FPU?

Is it possible to clear the FPU?

我正在使用 Delphi XE6 执行复杂的浮点计算。我意识到浮点数的局限性,因此理解浮点数固有的不准确性。但是在这种特殊情况下,我总是在计算结束时得到 2 个不同值中的 1 个。
第一个值,过了一会儿(我还没弄清楚为什么和什么时候),它翻转到第二个值,然后我无法再次获得第一个值,除非我重新启动我的应用程序。我真的不能更具体,因为计算非常复杂。我几乎可以理解这个值是否有点随机,但只有 2 个不同的状态有点令人困惑。这只发生在 32 位编译器中,无论我尝试多少次,64 位编译器都会给出一个单一的答案。这个数字与 32 位计算中的 2 不同,但我理解为什么会这样,我对此没有意见。我需要一致性,而不是完全准确。
我的一个怀疑是,也许 FPU 在经过一些影响后续计算的计算后处于一种状态,因此我的问题是关于清除所有寄存器和 FPU 堆栈以平衡竞争环境。在开始计算之前,我将其称为 CLEARFPU。

经过更多调查后,我意识到我找错了地方。你看到的不是浮点数得到的。我正在查看数字的字符串表示,并认为这里有 4 个数字进入计算全部相等,结果不同。事实证明,这些数字似乎只是相同的。我开始记录数字的十六进制等价物,返回并发现用于矩阵乘法的外部 dll 是错误的原因。我用 Delphi 中编写的例程替换了矩阵乘法,一切都很好。

浮点计算是确定性的。输入是输入数据和浮点控制字。使用相同的输入,相同的计算将产生可重复的输出。

如果你有不可预知的结果,那一定是有原因的。输入数据或浮点控制字在变化。你必须诊断那是什么原因。在您完全理解问题之前,您不应该寻找问题。不要在不了解疾病的情况下尝试使用橡皮膏。

所以下一步是在一段简单的代码中隔离和重现问题。一旦可以重现问题,就可以解决问题。

可能的解释包括使用未初始化的数据,或修改浮点控制字的外部代码。但可能还有其他原因。

未初始化的数据是合理的。更有可能是某些外部代码正在修改浮点控制字。检测您的代码以在执行的各个阶段记录浮点控制字,以查看它是否曾经意外更改。

您可能已经被优化和过高的 x87 FPU 精度组合所困扰,导致源代码中相同位的浮点代码被具有不同舍入行为的不同汇编代码实现复制。

x87 FPU 数学问题

基本问题是,虽然x87 FPU支持32位、64位和80位浮点值,但它只有80位寄存器,运算精度由寄存器的状态决定浮点控制字中的位,而不是使用的指令。更改舍入位的代价很高,因此大多数编译器不会这样做,因此无论涉及何种数据类型,所有浮点运算最终都以相同的精度执行。

因此,如果编译器将 FPU 设置为使用 80 位舍入并添加三个 64 位浮点变量,则生成的代码通常会添加前两个变量,将未舍入的结果保存在 80 位 FPU 寄存器中.然后它将第三个 64 位变量添加到寄存器中的 80 位值,从而在 FPU 寄存器中产生另一个未舍入的 80 位值。这可能会导致计算出不同的值,而不是在每一步后将结果四舍五入为 64 位精度。

如果结果值随后存储在 64 位浮点变量中,则编译器可能会将其写入内存,此时将其舍入为 64 位。但是,如果该值用于以后的浮点计算,那么编译器可能会将其保存在寄存器中。这意味着此时发生的舍入取决于编译器执行的优化。它越能够将值保存在 80 位 FPU 寄存器中以提高速度,如果根据代码中使用的实际浮点类型的大小对所有浮点运算进行四舍五入,结果与您得到的结果就会越不同。

为什么 SSE 浮点数学更好

对于 64 位代码,通常不使用 x87 FPU,而是使用等效的标量 SSE 指令。对于这些指令,所用操作的精度由所用指令决定。因此,对于添加三个数字的示例,编译器将发出使用 64 位精度添加数字的指令。结果是存储在内存中还是保留在寄存器中都没有关系,值保持不变,因此优化不会影响结果。

优化如何将确定性 FP 代码转变为非确定性 FP 代码

到目前为止,这可以解释为什么使用 32 位代码和 64 位代码会得到不同的结果,但不能解释为什么使用相同的 32 位代码会得到不同的结果。这里的问题是优化可以以令人惊讶的方式改变你的代码。编译器可以做的一件事是出于各种原因重复代码,这可能会导致相同的浮点代码在应用了不同优化的不同代码路径中执行。

由于优化会影响浮点结果,这意味着即使源代码中只有一个代码路径,不同的代码路径也会产生不同的结果。如果在 运行 时选择的代码路径是不确定的,那么即使源代码中的结果不依赖于任何非确定性因素,这也会导致不确定的结果。

一个例子

例如,考虑这个循环。它执行一个很长的 运行ning 计算,所以每隔几秒它就会打印一条消息,让用户知道到目前为止已经完成了多少次迭代。在循环的末尾,使用浮点运算执行简单的求和。虽然循环中存在不确定因素,但浮点运算并不依赖于它。无论是否打印进度更新,它始终执行。

while ... do
begin
    ...
    if TimerProgress() then
    begin
        PrintProgress(count);
        count := 0
    end
    else
        count := count + 1;
    sum := sum + value
end

作为优化,编译器可能会将最后一个求和语句移动到 if 语句的两个块的末尾。这让两个块都通过跳回到循环的开始来完成,从而节省了跳转指令。否则其中一个块必须以跳转到求和语句结束。

这会将代码转换为:

while ... do
begin
    ...
    if TimerProgress() then
    begin
        PrintProgress(count);
        count := 0;
        sum := sum + value
    end
    else
    begin
        count := count + 1;
        sum := sum + value
    end
end

这可能导致两个求和的优化方式不同。它可能在一个代码路径中变量 sum 可以保存在寄存器中,但在另一条路径中它被强制进入内存。如果此处使用 x87 浮点指令,这可能会导致 sum 根据不确定因素进行不同的舍入:是否是时候打印进度更新。

可能的解决方案

无论问题的根源是什么,清除 FPU 状态都无法解决问题。 64 位版本有效的事实提供了一个可能的解决方案,即使用 SSE 数学而不是 x87 数学。我不知道 Delphi 是否支持这个,但这是 C 编译器的共同特征。使基于 x87 的浮点数学符合 C 标准非常困难和昂贵,因此许多 C 编译器支持使用 SSE 数学。

不幸的是,在 Internet 上快速搜索表明 Delphi 编译器没有在 32 位代码中使用 SSE 浮点数学的选项。在那种情况下,您的选择将更加有限。您可以尝试禁用优化,这应该可以防止编译器创建相同代码的不同优化版本。您还可以尝试更改 x87 浮点控制字中的舍入精度。默认情况下,它使用 80 位精度,但所有浮点变量都是 64 位,然后将 FPU 更改为使用 64 位精度应该会显着降低优化对舍入的影响。

稍后你可以使用 Set8087CW procedure MBo mentioned, or maybe System.Math.SetPrecisionMode