Ste*_*eve 1 delphi floating-point fpu
我正在使用Delphi XE6执行复杂的浮点计算.我意识到浮点数的局限性,因此理解FP数中固有的不准确性.然而,在这种特殊情况下,我总是在计算结束时获得2个不同值中的1个.
第一个值和一段时间后(我还没弄清楚原因和时间),它翻转到第二个值,然后我再也无法获得第一个值,除非我重新启动我的应用程序.由于计算非常复杂,我无法更具体.我几乎可以理解这个值是否有点随机,但只有2个不同的状态有点令人困惑.这只发生在32位编译器中,无论我尝试多少次,64位编译器都会给出一个答案.这个数字与32位计算中的2不同,但我理解为什么会发生这种情况,我很好.我需要一致性,而不是完全准确性.
我唯一怀疑的是,在一些影响后续计算的计算之后,FPU可能会处于某种状态,因此我的问题是清除所有寄存器和FPU堆栈以平衡竞争场.在开始计算之前,我打电话给这个CLEARFPU.
经过一番调查,我意识到我在找错了地方.你看到的不是浮点数所得到的.我正在查看数字的字符串表示,并认为这里有4个数字进入计算ALL EQUAL,结果是不同的.事实证明这些数字似乎只是相同.我开始记录数字的十六进制等效值,回过头来找到一个外部dll,用于矩阵乘法导致错误.我用Delphi编写的例程替换了矩阵乘法,一切都很好.
浮点计算是确定性的.输入是输入数据和浮点控制字.使用相同的输入,相同的计算将产生可重复的输出.
如果你有不可预测的结果,那么就有理由.输入数据或浮点控制字是变化的.你必须诊断出这是什么原因.在您完全理解问题之前,您不应该寻找问题.在不了解疾病的情况下,不要尝试使用贴膏药.
因此,下一步是在一段简单的代码中隔离和重现问题.一旦您可以重现问题,您就可以解决问题.
可能的解释包括使用未初始化的数据或修改浮点控制字的外部代码.但可能还有其他原因.
未初始化的数据似乎是合理的.也许更有可能是某些外部代码正在修改浮点控制字.检测代码以在执行的各个阶段记录浮点控制字,以查看它是否会意外更改.
您可能已经被优化和过度的 x87 FPU 精度组合所困扰,导致源代码中的同一位浮点代码被具有不同舍入行为的不同汇编代码实现重复。
基本问题是,虽然x87 FPU支持32位、64位和80位浮点值,但它只有80位寄存器,运算精度由浮点中位的状态决定控制字,而不是使用的指令。更改舍入位的成本很高,因此大多数编译器不会这样做,因此无论涉及哪种数据类型,所有浮点运算最终都会以相同的精度执行。
因此,如果编译器将 FPU 设置为使用 80 位舍入,并且添加三个 64 位浮点变量,则生成的代码通常会添加前两个变量,并将未舍入的结果保留在 80 位 FPU 寄存器中。然后,它将第三个 64 位变量添加到寄存器中的 80 位值,从而在 FPU 寄存器中产生另一个未舍入的 80 位值。与每个步骤后将结果四舍五入到 64 位精度相比,这可能会导致计算出不同的值。
如果该结果值随后存储在 64 位浮点变量中,则编译器可能会将其写入内存,此时将其舍入为 64 位。但是,如果该值用于以后的浮点计算,则编译器可能会将其保留在寄存器中。这意味着此时发生的舍入取决于编译器执行的优化。为了提高速度而将值保存在 80 位 FPU 寄存器中的能力越强,如果所有浮点运算都根据代码中使用的实际浮点类型的大小进行舍入,则结果与所得到的结果的差异就越大。
对于 64 位代码,通常不使用 x87 FPU,而是使用等效的标量 SSE 指令。对于这些指令,所使用的运算精度由所使用的指令决定。因此,对于三个数字相加的示例,编译器将发出使用 64 位精度将数字相加的指令。无论结果存储在内存中还是保留在寄存器中,该值都保持不变,因此优化不会影响结果。
到目前为止,这可以解释为什么使用 32 位代码和 64 位代码会得到不同的结果,但它并不能解释为什么使用相同的 32 位代码会得到不同的结果。这里的问题是优化可能会以令人惊讶的方式改变你的代码。编译器可以做的一件事是出于各种原因重复代码,这可能会导致相同的浮点代码在应用了不同优化的不同代码路径中执行。
由于优化会影响浮点结果,这可能意味着即使源代码中只有一个代码路径,不同的代码路径也会给出不同的结果。如果在运行时选择的代码路径是不确定的,那么即使在源代码中结果不依赖于任何非确定性因素,这也可能导致不确定的结果。
例如,考虑这个循环。它执行长时间运行的计算,因此每隔几秒它就会打印一条消息,让用户知道到目前为止已经完成了多少次迭代。在循环结束时,使用浮点算术执行简单求和。虽然循环中存在不确定性因素,但浮点运算并不依赖于它。无论是否打印更新的进度,它总是会执行。
while ... do
begin
...
if TimerProgress() then
begin
PrintProgress(count);
count := 0
end
else
count := count + 1;
sum := sum + value
end
Run Code Online (Sandbox Code Playgroud)
作为优化,编译器可能会将最后一个求和语句移到 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
Run Code Online (Sandbox Code Playgroud)
这可能会导致两个求和的优化方式不同。可能在一个代码路径中,变量sum可以保存在寄存器中,但在另一路径中,变量会被强制保存到内存中。如果此处使用 x87 浮点指令,这可能会导致sum根据非确定性因素进行不同的舍入:是否是打印进度更新的时间。
无论问题的根源是什么,清除 FPU 状态并不能解决问题。事实上,64 位版本可以工作,这提供了一个可能的解决方案,即使用 SSE 数学而不是 x87 数学。我不知道Delphi是否支持这个,但这是C编译器的共同特性。让基于 x87 的浮点数学符合 C 标准非常困难且昂贵,因此许多 C 编译器支持使用 SSE 数学。
不幸的是,在互联网上的快速搜索表明 Delphi 编译器没有在 32 位代码中使用 SSE 浮点数学的选项。在这种情况下,你的选择将会更加有限。您可以尝试禁用优化,这应该会阻止编译器创建同一代码的不同优化版本。您还可以尝试更改 x87 浮点控制字中的舍入精度。默认情况下,它使用 80 位精度,但所有浮点变量都是 64 位,然后将 FPU 更改为使用 64 位精度应该会显着减少优化对舍入的影响。
要执行后面的操作,您可能可以使用提到的Set8087CW过程 MBo ,或者System.Math.SetPrecisionMode。
| 归档时间: |
|
| 查看次数: |
689 次 |
| 最近记录: |