Visual C++ 2012 中的正弦计算不一致?

Posted

技术标签:

【中文标题】Visual C++ 2012 中的正弦计算不一致?【英文标题】:Sine computation inconsistency in Visual C++ 2012? 【发布时间】:2012-09-01 07:01:45 【问题描述】:

考虑以下代码:

// Filename fputest.cpp

#include <cmath>
#include <cstdio>

int main()

    double x;
    *(__int64 *) &x = 0xc01448ec3aaa278di64; // -5.0712136427263319
    double sine1 = sin(x);
    printf("%016llX\n", sine1);
    double sine2;
    __asm 
    fld x
    fsin
    fstp sine2
    
    printf("%016llX\n", sine2);
    return 0;

使用 Visual C++ 2012 (cl fputest.cpp) 编译并执行程序时,输出如下:

3FEDF640D8D36174
3FEDF640D8D36175

问题:

为什么这两个值不同? 是否可以发布一些编译器选项以使计算的正弦值完全相同?

【问题讨论】:

一个操作可以完全在浮点寄存器中进行。当 80 位寄存器写入 64 位内存地址时,另一个会导致精度损失。 FSTP 文档说“将值存储在内存中时,会将值转换为单实数或双实数格式。” fsin 方法使用具有 80 位精度的 x87 FPU,sin 在 MSVC(我使用的是 2010)中的实现似乎使用具有 128 位 xmm* 寄存器的 SSE . (另见this question。) 【参考方案1】:

这个问题不是由 long double 到 double 的转换引起的。这可能是由于数学库中的sin 例程不准确。

fsin 指令被指定为在其范围内的操作数(根据 Intel 64 和 IA-32 架构软件开发人员手册,2011 年 10 月,第 1 卷,第 8.3 卷)生成 1 ULP(长双精度格式)内的结果。 10),在四舍五入到最近的模式。在 Intel Core i7 上,fsin 的提问者的值,-5.07121364272633190495298549649305641651153564453125 或 -0x1.448ec3aaa278dp+2,产生 0xe.fb206c69b0ba402p-4。我们可以很容易地从这个十六进制看到最后 11 位是 100 0000 0010。这些是从 long double 转换时将被舍入的位。如果它们大于 100 0000 0000,则数字将向上取整。他们更大。因此,将这个 long double 值转换为 double 的结果是 0xe.fb206c69b0ba8p-4,等于 0x1.df640d8d36175p-1 和 0.93631021832247418590355891865328885614871978759765625。另请注意,即使结果低一个 ULP,最后 11 位仍将大于 100 0000 0000 并且仍会向上取整。因此,此结果在符合上述文档的 Intel CPU 上应该不会有所不同。

将此与直接计算双精度正弦进行比较,使用产生正确舍入结果的理想sin 例程。的值的正弦近似0.93631021832247413051857150785044253634581268961333520518023697738674775240815140702992025520721336793516756640679315765619707343171517531053811196321335899848286682535203710849065933755262347468763562(的Maple 10计算的)。最接近此的双精度为 0x1.df640d8d36175p-1。这与我们将 fsin 结果转换为 double 获得的值相同。

因此,差异不是由 long double 转换为 double 引起的;将 long double fsin 结果转换为 double 会产生与理想双精度 sin 例程完全相同的结果。

对于提问者的 Visual Studio 包使用的 sin 例程的准确性,我们没有规范。在商业图书馆中,允许 1 个 ULP 或多个 ULP 的错误是很常见的。观察正弦与双精度值舍入的点有多接近:距双精度值的距离为 0.498864 ULP(双精度 ULP),因此距舍入更改的点为 0.001136 ULP。因此,即使sin 例程中的一个非常轻微的错误也会导致它返回 0x1.df640d8d36174p-1 而不是更接近的 0x1.df640d8d36175p-1。

因此,我推测差异的根源是sin 例程中的一个非常小的错误。

【讨论】:

【参考方案2】:

(注意:如 cmets 中所述,这在 VC2012 上不起作用。我把它留在这里作为一般信息。我不建议依赖任何依赖于优化级别的东西!)

我没有 VS2012,但是在 VS2010 编译器上,你可以在命令行上指定 /fp:fast,然后我得到相同的结果。这会导致编译器生成“快速”代码,这些代码不一定完全遵循 C++ 中所需的舍入和规则,但与您的汇编语言计算相匹配。

我不能在 VS2012 中尝试这个,但我想它有相同的选项。

这似乎也只能在优化的构建中使用/Ox 作为选项。

【讨论】:

【参考方案3】:

见Why is cos(x) != cos(y) even though x == y?

作为David mentioned in the comment,差异来自将 FP 寄存器中的数据移动到不同大小的内存位置(寄存器/ram)。而且也不总是分配;即使是另一个附近的浮点操作也足以刷新 FP 寄存器,从而使任何试图保证特定值的尝试都徒劳无功。如果您需要进行比较,您可以通过将所有结果强制到内存位置来缓解其中的一些问题,如下所示:

float F1 = sin(a); float F2 = sin(b); if (F1 == F2)

但是,即使这样也可能行不通。最好的方法是接受任何浮点运算只会“大部分准确”,并且从程序员的角度来看,即使重复执行相同的运算,这个错误实际上也是不可预测的和随机的。而不是

if (F1 == F2)

你应该使用一些效果

if (isArbitrarilyClose(F1, F2))

if (absf(F1 - F2) <= n)

n 是一个很小的数字。

【讨论】:

My answer 证明这是不正确的:在这种情况下,将 long-double fsin 结果舍入为 double 不会产生不正确的值或与计算正确舍入的 @ 的结果不同的值987654330@直接。 浮点计算可能在某些甚至大多数情况下是一致的;但是,您必须非常小心,即使如此,编译器仍可能会阻碍您的努力。最好还是做好最坏的打算。【参考方案4】:

VS2012 中的代码生成器进行了重大更改以支持automatic vectorization。部分更改是 x86 浮点数学现在在 SSE2 中完成,不再使用 FPU,这是必要的,因为 FPU 代码无法矢量化。 SSE2 使用 64 位精度而不是 80 位精度进行计算,因此很有可能由于四舍五入,结果相差一位。也是@J99在VS2010中可以和/fp:fast得到一致结果的原因,它的编译器仍然使用FPU,/fp:fast直接使用FSIN结果。

此功能非常有用,请查看链接网址中 Jim Hogg 的视频,了解如何利用它。

【讨论】:

64 位精度并不意味着 50% 的分布不正确。诸如此类的超越例程通常首先计算近似多项式的较低量值部分,最后添加最大分量,从而产生在很长一段时间内可以正确四舍五入的结果。

以上是关于Visual C++ 2012 中的正弦计算不一致?的主要内容,如果未能解决你的问题,请参考以下文章

visual studio 2012 c++ hello world - iostream 不工作

在 Visual Studio 2012 中为我的 c++ 项目设置 Aquila

Windows 窗体应用程序:访问其他窗体中的元素 Visual Studio 2012 C++

Visual C++ 中的去虚拟化

在 Visual Studio 2010 (C++) 中集成 MATLAB 代码

visual c++已经得到一个信号的波形,怎样检测波峰和波谷?