是否可以清除FPU?

时间:2015-11-04 17:03:51

标签: delphi floating-point fpu

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

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

2 个答案:

答案 0 :(得分:8)

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

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

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

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

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

答案 1 :(得分:4)

您可能已经被优化和过多的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位获得不同的结果码。这里的问题是优化可以以令人惊讶的方式改变您的代码。编译器可以做的一件事是由于各种原因重复代码,这可能导致在不同的代码路径中执行相同的浮点代码并应用不同的优化。

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

一个例子

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

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数学。

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

要做到以后你可以使用提到的Set8087CW程序,或者System.Math.SetPrecisionMode