不同程序中具有相同输入的相同双和代码的不同舍入行为(精度较低)
Different rounding behaviour (less precision) for the same double sum code with the same inputs in a different program
Visual Studio 2017 .Net 4.7.2 x86
在一个大型应用程序中,我们遇到了一个简单的双精度加法问题,这不是要解决的双精度问题,因为相同的代码在另一个应用程序或大型应用程序的另一部分中工作。
编辑:见C#+对应的汇编代码(在VS中右击去反汇编)
错误代码:
{
15B185FB nop
double aa = 44537.5703125d;
15B185FC fld dword ptr ds:[15B188F8h]
15B18602 fstp qword ptr [ebp-11Ch] // [ebp-11Ch] = 44537.5703125
double bb = aa + 120 / (3600.0 * 24.0);
15B18608 fld qword ptr [ebp-11Ch] // [ebp-11Ch] = 44537.5703125
15B1860E fadd qword ptr ds:[15B18900h] // 15B18900h = 0.0013888888888888889
15B18614 fstp qword ptr [ebp-124h] //[ebp-124h] = 44537.570312500000
}
15B1861A nop
好的代码:
double newMinimum = 44537.5703125d;
01870997 fld dword ptr ds:[18709E8h] // 0x009D09E8 => float 44537.5703
0187099D fstp qword ptr [ebp-48h] // 44537.570312500000
double newMaximum = newMinimum + 120 / (3600.0 * 24.0);
018709A0 fld qword ptr [ebp-48h] // [ebp-48h] = 44537.570312500000
018709A3 fadd qword ptr ds:[18709F0h] // 18709F0h = 0.0013888888888888889
018709A9 fstp qword ptr [ebp-50h] // [ebp-50h] = 44537.571701388886
错误的结果和是 44537.5703125,就像操作是用 float 精度完成的。
在@PeterCordes 回答后编辑:
现在我可以做一个最小的重复错误:
using SharpDX.Direct3D9;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BugDoublePrecision
{
class Bug
{
public static void Test(IntPtr handle)
{
precisionTest();
PresentParameters presentParameters = new PresentParameters()
{
Windowed = true,
SwapEffect = SwapEffect.Discard,
EnableAutoDepthStencil = true,
AutoDepthStencilFormat = Format.D16,
MultiSampleType = MultisampleType.FourSamples
};
var device = new Device(new Direct3D(), 0, DeviceType.Hardware, handle,
CreateFlags.SoftwareVertexProcessing /*| CreateFlags.FpuPreserve*/, presentParameters);
precisionTest(); //failed if FpuPreserve it not used
}
private static void precisionTest()
{
double aa = 44537.5703125d;
double bb = aa + 120 / (3600.0 * 24.0);
if (aa == bb)
System.Diagnostics.Debugger.Break();
}
}
}
两次都一样吧? (除了局部变量在堆栈帧中的位置。)因此,除了舍入模式和精度控制之外,对于相同的输入,它的行为相同。
一种可能的解释是某些东西将 x87 FPU 的精度控制设置为 24 位尾数精度(与 float
相同,但仍具有完整的指数范围);显然 D3D9 did/does 在 Windows 上这样做,因为它使 fsqrt
快一点。 Bruce Dawson 在他的 excellent 系列文章中解释了关于浮点理论和实践的文章,重点是 x86,尤其是 Windows:https://randomascii.wordpress.com/2012/03/21/intermediate-floating-point-precision/
(顺便说一句,x87 precision/rounding 控制寄存器是每个线程的事情,但 IDK 在启动新线程时它是如何继承的。显然“初始化 D3D”是有问题的代码运行的地方(可能通过本机-来自 C# 的代码接口);如果在此之后重置 x87 FPU,例如 finit
,您应该返回到完全中间精度。(通过另一个本机代码调用)。或者将其设置为 53 位尾数精度以避免在涉及存储为双精度的计算中进行多个舍入步骤,只需始终将所有内容舍入到该尾数宽度,就像使用 double
.)
的 SSE2 一样
如果可以,放弃过时的 x87 并让您的编译器像在 64 位模式下一样将 SSE2 用于标量双精度数学。它对浮点数和双精度数有单独的数学指令,而不是在 load/store 上进行转换。 SSE2 是 x86-64 的基线,存在于 P4 上;没有它的最古老的主流CPU是Athlon XP,尽管AMD Geode超低功耗CPU根本没有SIMD。
Visual Studio 2017 .Net 4.7.2 x86
在一个大型应用程序中,我们遇到了一个简单的双精度加法问题,这不是要解决的双精度问题,因为相同的代码在另一个应用程序或大型应用程序的另一部分中工作。
编辑:见C#+对应的汇编代码(在VS中右击去反汇编)
错误代码:
{
15B185FB nop
double aa = 44537.5703125d;
15B185FC fld dword ptr ds:[15B188F8h]
15B18602 fstp qword ptr [ebp-11Ch] // [ebp-11Ch] = 44537.5703125
double bb = aa + 120 / (3600.0 * 24.0);
15B18608 fld qword ptr [ebp-11Ch] // [ebp-11Ch] = 44537.5703125
15B1860E fadd qword ptr ds:[15B18900h] // 15B18900h = 0.0013888888888888889
15B18614 fstp qword ptr [ebp-124h] //[ebp-124h] = 44537.570312500000
}
15B1861A nop
好的代码:
double newMinimum = 44537.5703125d;
01870997 fld dword ptr ds:[18709E8h] // 0x009D09E8 => float 44537.5703
0187099D fstp qword ptr [ebp-48h] // 44537.570312500000
double newMaximum = newMinimum + 120 / (3600.0 * 24.0);
018709A0 fld qword ptr [ebp-48h] // [ebp-48h] = 44537.570312500000
018709A3 fadd qword ptr ds:[18709F0h] // 18709F0h = 0.0013888888888888889
018709A9 fstp qword ptr [ebp-50h] // [ebp-50h] = 44537.571701388886
错误的结果和是 44537.5703125,就像操作是用 float 精度完成的。
在@PeterCordes 回答后编辑: 现在我可以做一个最小的重复错误:
using SharpDX.Direct3D9;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BugDoublePrecision
{
class Bug
{
public static void Test(IntPtr handle)
{
precisionTest();
PresentParameters presentParameters = new PresentParameters()
{
Windowed = true,
SwapEffect = SwapEffect.Discard,
EnableAutoDepthStencil = true,
AutoDepthStencilFormat = Format.D16,
MultiSampleType = MultisampleType.FourSamples
};
var device = new Device(new Direct3D(), 0, DeviceType.Hardware, handle,
CreateFlags.SoftwareVertexProcessing /*| CreateFlags.FpuPreserve*/, presentParameters);
precisionTest(); //failed if FpuPreserve it not used
}
private static void precisionTest()
{
double aa = 44537.5703125d;
double bb = aa + 120 / (3600.0 * 24.0);
if (aa == bb)
System.Diagnostics.Debugger.Break();
}
}
}
两次都一样吧? (除了局部变量在堆栈帧中的位置。)因此,除了舍入模式和精度控制之外,对于相同的输入,它的行为相同。
一种可能的解释是某些东西将 x87 FPU 的精度控制设置为 24 位尾数精度(与 float
相同,但仍具有完整的指数范围);显然 D3D9 did/does 在 Windows 上这样做,因为它使 fsqrt
快一点。 Bruce Dawson 在他的 excellent 系列文章中解释了关于浮点理论和实践的文章,重点是 x86,尤其是 Windows:https://randomascii.wordpress.com/2012/03/21/intermediate-floating-point-precision/
(顺便说一句,x87 precision/rounding 控制寄存器是每个线程的事情,但 IDK 在启动新线程时它是如何继承的。显然“初始化 D3D”是有问题的代码运行的地方(可能通过本机-来自 C# 的代码接口);如果在此之后重置 x87 FPU,例如 finit
,您应该返回到完全中间精度。(通过另一个本机代码调用)。或者将其设置为 53 位尾数精度以避免在涉及存储为双精度的计算中进行多个舍入步骤,只需始终将所有内容舍入到该尾数宽度,就像使用 double
.)
如果可以,放弃过时的 x87 并让您的编译器像在 64 位模式下一样将 SSE2 用于标量双精度数学。它对浮点数和双精度数有单独的数学指令,而不是在 load/store 上进行转换。 SSE2 是 x86-64 的基线,存在于 P4 上;没有它的最古老的主流CPU是Athlon XP,尽管AMD Geode超低功耗CPU根本没有SIMD。