我正在使用德尔菲XE6执行一个复杂的浮点计算。我认识到浮点数的局限性,因此理解FP数字固有的不准确之处。然而,在这个特殊情况下,我总是在计算结束时得到两个不同的值中的一个。
第一个值和一段时间后(我还不知道原因和时间),它会切换到第二个值,然后除非重新启动应用程序,否则就无法再次获得第一个值。我不能说得更具体了,因为计算非常复杂。如果这个值是随机的,我几乎可以理解,但是只有两个不同的状态有点让人困惑。这种情况只发生在32位编译器中,无论我尝试了多少次,64位编译器都会给出一个单一的答案。这个数字与32位计算中的2不同,但我理解为什么会发生这种情况,我对此没有意见。我需要一致,而不是完全准确。
我的一项怀疑是,在计算后,FPU可能处于一种状态,这会影响随后的计算,因此,我的问题是清除所有寄存器和FPU堆栈,以平衡竞争环境。在开始计算之前,我称之为CLEARFPU。
经过进一步的调查,我意识到我找错地方了。你所看到的并不是你用浮点数得到的。我看了数字的字符串表示,认为这里有4个数字,计算结果都是相等的,结果是不同的。结果似乎只有相同的数字。我开始记录数字的十六进制等价物,然后用我的方式返回,并找到一个外部dll,用于矩阵乘法--导致错误的原因。我用Delphi写的例程代替了矩阵乘法,一切都很好。
发布于 2015-11-04 17:34:22
浮点计算是确定性的。输入是输入数据和浮点控制字。对于相同的输入,相同的计算将产生可重复的输出。
如果你有不可预测的结果,那么就会有一个原因。输入数据或浮点控制字都是可变的。你必须诊断出这是什么原因。在你完全理解这个问题之前,你不应该去寻找问题。在不了解疾病的情况下,不要尝试贴石膏。
所以下一步是用一段简单的代码来隔离和再现这个问题。一旦你能重现这个问题,你就能解决这个问题。
可能的解释包括使用未初始化的数据,或修改浮点控制字的外部代码。但可能还有其他原因。
未初始化的数据是可信的。也许更有可能的是,一些外部代码正在修改浮点控制字。测试您的代码,在执行的各个阶段记录浮点控制字,看看它是否有意外的变化。
发布于 2015-11-04 20:33:54
您可能被优化和过量的x87 FPU精度结合在一起,导致源代码中相同的浮点代码被重复使用不同的汇编码实现,并具有不同的舍入行为。
x87 FPU数学问题
基本问题是,虽然x87 FPU支持32位、64位和80位浮点值,但它只有80位寄存器,操作精度取决于浮点控制字中位的状态,而不是所使用的指令。更改舍入位是昂贵的,所以大多数编译器不会,因此所有浮点操作都是以相同的精度执行,而不管涉及的数据类型如何。
因此,如果编译器将FPU设置为使用80位舍入,然后添加三个64位浮点变量,生成的代码通常会将前两个变量添加到一个80位FPU寄存器中保存未舍入结果。然后,它将向寄存器中的80位值中添加第三个64位变量,从而在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过程MBo,也可以使用System.Math.SetPrecisionMode。
https://stackoverflow.com/questions/33527760
复制相似问题