启用优化的不同浮点结果-编译器错误?


109

以下代码在经过优化和未经优化的情况下均适用于Visual Studio 2008。但是,它仅适用于未经优化(O0)的g ++。

#include <cstdlib>
#include <iostream>
#include <cmath>

double round(double v, double digit)
{
    double pow = std::pow(10.0, digit);
    double t = v * pow;
    //std::cout << "t:" << t << std::endl;
    double r = std::floor(t + 0.5);
    //std::cout << "r:" << r << std::endl;
    return r / pow;
}

int main(int argc, char *argv[])
{
    std::cout << round(4.45, 1) << std::endl;
    std::cout << round(4.55, 1) << std::endl;
}

输出应为:

4.5
4.6

但是带有优化(O1- O3)的g ++ 将输出:

4.5
4.5

如果我volatile在t之前添加关键字,那么它可以工作,那么是否可能存在某种优化错误?

在g ++ 4.1.2和4.4.4上进行测试。

这是在ideone上的结果:http ://ideone.com/Rz937

我在g ++上测试的选项很简单:

g++ -O2 round.cpp

更有趣的结果是,即使我/fp:fast在Visual Studio 2008上打开了选项,结果仍然是正确的。

进一步的问题:

我想知道,我应该始终打开该-ffloat-store选项吗?

因为我测试的g ++版本CentOS / Red Hat Linux 5和CentOS / Redhat 6一起提供

我在这些平台上编译了许多程序,但我担心这会导致程序内部出现意外错误。研究我所有的C ++代码和使用的库是否存在此类问题似乎有点困难。有什么建议吗?

是否有人对为什么即使/fp:fast打开Visual Studio 2008仍然可以工作感兴趣?似乎Visual Studio 2008在此问题上比g ++更可靠?


51
对于所有新的SO用户:这就是您提出问题的方式。+1
tenfour 2011年

1
FWIW,我使用MinGW在g ++ 4.5.0中获得了正确的输出。
史蒂夫·布莱克韦尔

2
ideone使用4.3.4 ideone.com/b8VXg
Daniel A. White,

5
您应该记住,您的例程不太可能在各种输出中可靠地工作。与将双精度数四舍五入为整数相比,这容易受到以下事实的影响:并非所有实数都可以表示,因此您应该期望得到更多像这样的错误。
Jakub Wieczorek

2
对于那些无法重现该错误的人:不要取消注释已注释掉的调试stmts,它们会影响结果。
n。代词

Answers:


91

英特尔x86处理器内部使用80位扩展精度,而double通常为64位宽。不同的优化级别会影响来自CPU的浮点值保存到内存的频率,从而从80位精度四舍五入到64位精度。

使用-ffloat-storegcc选项可获得具有不同优化级别的相同浮点结果。

或者,使用long double通常在gcc上为80位宽的类型,以避免精度从80位舍入到64位。

man gcc 全部说明:

   -ffloat-store
       Do not store floating point variables in registers, and inhibit
       other options that might change whether a floating point value is
       taken from a register or memory.

       This option prevents undesirable excess precision on machines such
       as the 68000 where the floating registers (of the 68881) keep more
       precision than a "double" is supposed to have.  Similarly for the
       x86 architecture.  For most programs, the excess precision does
       only good, but a few programs rely on the precise definition of
       IEEE floating point.  Use -ffloat-store for such programs, after
       modifying them to store all pertinent intermediate computations
       into variables.

在x86_64的构建编译器使用SSE寄存器floatdouble默认情况下,所以没有扩展精度使用,不会出现此问题。

gcc编译器选项-mfpmath对此进行控制。


20
我认为这就是答案。常量4.55转换为4.54999999999999,它是最接近的64位二进制表示形式;乘以10并再次舍入为64位,您将得到45.5。如果通过将舍入步骤保留在80位寄存器中而跳过了舍入步骤,则最终结果为45.4999999999999。
Mark Ransom

谢谢,我什至不知道这个选项。但是我想知道,是否应该始终打开-ffloat-store选项?因为我测试的g ++版本是随CentOS / Redhat 5和CentOS / Redhat 6一起提供的,所以我在这些平台上编译了许多程序,因此我担心这会导致程序内部出现意外错误。

5
@熊,调试语句可能导致该值从寄存器刷新到内存中。
Mark Ransom

2
@Bear,通常,您的应用程序应该受益于扩展的精度,除非当预期64位浮点数会产生不足或溢出时,它会在非常小的值或巨大的值上运行inf。没有好的经验法则,单元测试可以为您提供明确的答案。
Maxim Egorushkin 2011年

2
@bear作为一般规则,如果您需要可完全预测的结果和/或人类将在纸上做的总和,那么您应该避免浮点数。-ffloat-store消除了不可预测性的一个来源,但这不是魔术。
–plugwash

10

输出应该是:4.5 4.6如果您具有无限的精度,或者正在使用使用基于十进制而不是基于二进制的浮点表示的设备,那么输出就是这样。但是,事实并非如此。大多数计算机使用二进制IEEE浮点标准。

正如Maxim Yegorushkin在回答中指出的那样,部分问题是计算机内部使用的是80位浮点表示形式。不过,这只是问题的一部分。问题的基础是,任何形式的n.nn5都没有确切的二进制浮点表示形式。这些极端情况总是不精确的数字。

如果您确实希望舍入能够可靠地舍入这些极端情况,则需要一种舍入算法来解决以下事实:n.n5,n.nn5或n.nnn5等(而不是n.5)始终是不精确。查找确定某些输入值是向上​​舍入还是向下舍入的特殊情况,并根据与该特殊情况的比较返回四舍五入后的值。而且,您确实需要注意,优化的编译器不会将找到的特殊情况放入扩展的精度寄存器中。

请参阅即使不精确,Excel如何如何成功舍入浮点数?对于这样的算法。

或者,您可以忍受极端情况有时会错误舍入的事实。


6

不同的编译器具有不同的优化设置。根据IEEE 754的规定,某些较快的优化设置未维护严格的浮点规则。Visual Studio中有一个特定的设置,/fp:strict/fp:precise/fp:fast,其中/fp:fast违反了什么可以做标准。您可能会发现标志是控制此类设置中的优化的因素。您可能还会在GCC中找到类似的设置,该设置会更改行为。

如果是这种情况,则编译器之间的唯一不同之处在于,默认情况下,GCC会在较高的优化条件下寻找最快的浮点行为,而Visual Studio不会在较高的优化级别上更改浮点行为。因此,它不一定是实际的错误,而是您不知道要打开的选项的预期行为。


4
自从引用以来-ffast-math,有一个针对GCC 的 开关,并且没有通过任何-O优化级别将其打开:“它可能导致程序的输出不正确,这些程序取决于对数学函数的IEEE或ISO规则/规范的确切实现。”
马太福音

@Mat:我已经尝试过,-ffast-math并且还进行了其他操作g++ 4.4.3,但仍然无法重现该问题。
NPE

尼斯与-ffast-math我得到4.5在这两种情况下的优化级别大于0
Kerrek SB 2011年

(更正我得到4.5-O1-O2,但不会-O0-O3在GCC 4.4.3,但-O1,2,3在GCC 4.6.1。)
Kerrek SB

4

对于那些无法重现该错误的人:不要取消注释已注释掉的调试stmts,它们会影响结果。

这意味着问题与调试语句有关。看起来由于在输出语句期间将值加载到寄存器中而导致舍入错误,这就是为什么其他人发现您可以使用以下方法解决此问题的原因-ffloat-store

进一步的问题:

我想知道,我应该一直打开-ffloat-store选项吗?

坦率地说,一定有一些程序员没有打开的原因-ffloat-store,否则该选项将不存在(同样,一定有一些程序员没有打开的原因-ffloat-store)。我不建议始终打开或关闭它。启用它会阻止一些优化,但是将其禁用可以使您获得某种行为。

但是,通常,二进制浮点数(例如计算机使用的)和十进制浮点数(人们熟悉的)之间存在一些不匹配,并且这种不匹配会导致与您得到的行为类似的行为(很明显,该行为你得到的是没有造成这种不匹配,但类似的行为可能是)。事实是,由于您在处理浮点数时已经有些模糊,我不能说这-ffloat-store会使它变得更好或更糟。

取而代之的是,您可能想研究要解决的问题的其他解决方案(不幸的是,Koenig并未指向实际的论文,而且我无法为其找到一个明显的“规范”位置,因此,我必须将您发送到Google)。


如果您不是出于输出目的而四舍五入,那么我可能会看std::modf()(in cmath)和std::numeric_limits<double>::epsilon()(in limits)。考虑到原始round()函数,我相信将其替换std::floor(d + .5)为对此函数的调用会更干净:

// this still has the same problems as the original rounding function
int round_up(double d)
{
    // return value will be coerced to int, and truncated as expected
    // you can then assign the int to a double, if desired
    return d + 0.5;
}

我认为这表明以下改进:

// this won't work for negative d ...
// this may still round some numbers up when they should be rounded down
int round_up(double d)
{
    double floor;
    d = std::modf(d, &floor);
    return floor + (d + .5 + std::numeric_limits<double>::epsilon());
}

一个简单的注释: std::numeric_limits<T>::epsilon()定义为“添加到1的最小数字创建的数字不等于1。” 通常,您需要使用相对epsilon(即以某种方式缩放epsilon来说明您使用的数字不是“ 1”)。的总和d.5并且std::numeric_limits<double>::epsilon()应该接近1,因此分组,添加方式是std::numeric_limits<double>::epsilon()将有关的权利的大小,我们正在做的事情。如果有的话,std::numeric_limits<double>::epsilon()它将太大(当所有三个的总和小于一时),并且可能导致我们在不应该的时候将一些数字四舍五入。


如今,您应该考虑std::nearbyint()


“相对ε”称为1 ulp(最后一个单位)。 x - nextafter(x, INFINITY)与x的1 ulp有关(但不要使用它;我确定有拐角情况,我只是把它补上了)。的cppreference示例epsilon() 包含对其进行缩放以获取基于ULP的相对误差的示例
彼得·科德斯

2
顺便说一句,2016年的答案-ffloat-store是:首先不要使用x87。使用SSE2数学(64位二进制文​​件,或-mfpmath=sse -msse2用于制作顽固的旧32位二进制文​​件),因为SSE ​​/ SSE2的临时文件没有额外的精度。 XMM寄存器中的var doublefloatvar实际上是IEEE 64位或32位格式。(与x87不同,x87的寄存器始终为80位,并且存储到内存的位
舍入

3

如果要编译到不包含SSE2的x86目标,则可接受的答案是正确的。所有现代的x86处理器都支持SSE2,因此,如果可以利用它,则应该:

-mfpmath=sse -msse2 -ffp-contract=off

让我们分解一下。

-mfpmath=sse -msse2。这通过使用SSE2寄存器执行舍入,这比将每个中间结果存储到内存要快得多。请注意,对于x86-64 ,这已经是 GCC上的默认设置。从GCC Wiki

在支持SSE2的现代x86处理器上,指定编译器选项-mfpmath=sse -msse2可确保所有浮点和双精度操作均在SSE寄存器中执行并正确取整。这些选项不会影响ABI,因此应尽可能用于可预测的数值结果。

-ffp-contract=off。但是,仅对四舍五入进行精确匹配是不够的。FMA(融合乘加)指令可以更改舍入行为,而不是非融合指令,因此我们需要将其禁用。这是Clang而非GCC的默认设置。如此答案所解释:

FMA仅进行一次舍入(有效保持内部临时乘法结果的无限精度),而ADD + MUL进行两次舍入。

通过禁用FMA,我们得到的结果在调试和发布时完全匹配,但会牺牲一些性能(和准确性)。我们仍然可以利用SSE和AVX的其他性能优势。


1

我对这个问题进行了更多的研究,可以提高精度。首先,以下是根据x84_64上gcc的4.45和4.55的精确表示形式(使用libquadmath来打印最后的精度):

float 32:   4.44999980926513671875
double 64:  4.45000000000000017763568394002504646778106689453125
doublex 80: 4.449999999999999999826527652402319290558807551860809326171875
quad 128:   4.45000000000000000000000000000000015407439555097886824447823540679418548304813185723105561919510364532470703125

float 32:   4.55000019073486328125
double 64:  4.54999999999999982236431605997495353221893310546875
doublex 80: 4.550000000000000000173472347597680709441192448139190673828125
quad 128:   4.54999999999999999999999999999999984592560444902113175552176459320581451695186814276894438080489635467529296875

如上面的Maxim所述,问题是由于FPU寄存器的80位大小引起的。

但是,为什么问题在Windows上永远不会发生?在IA-32上,x87 FPU被配置为对53位尾数使用内部精度(相当于64位的总大小:)double。对于Linux和Mac OS,使用的默认精度为64位(等于80位的总大小:)long double。因此,通过更改FPU的控制字,在这些不同的平台上是否可能出现问题(假设指令序列将触发该错误)。该问题已报告给gcc,为bug 323(至少请阅读注释92!)。

为了显示Windows的尾数精度,您可以使用VC ++将其编译为32位:

#include "stdafx.h"
#include <stdio.h>  
#include <float.h>  

int main(void)
{
    char t[] = { 64, 53, 24, -1 };
    unsigned int cw = _control87(0, 0);
    printf("mantissa is %d bits\n", t[(cw >> 16) & 3]);
}

在Linux / Cygwin上:

#include <stdio.h>

int main(int argc, char **argv)
{
    char t[] = { 24, -1, 53, 64 };
    unsigned int cw = 0;
    __asm__ __volatile__ ("fnstcw %0" : "=m" (*&cw));
    printf("mantissa is %d bits\n", t[(cw >> 8) & 3]);
}

请注意,使用gcc可以通过设置FPU精度-mpc32/64/80,尽管在Cygwin中会忽略它。但请记住,它将改变尾数的大小,但不会改变指数尾数的大小,这将使其他类型的行为有所变化。

在x86_64架构上,如tmandry所述使用SSE ,因此除非您使用强制将旧的x87 FPU用于FP计算-mfpmath=387,或者除非您使用32位模式进行编译-m32(您将需要multilib软件包),否则不会发生此问题。我可以在Linux上用标志和gcc版本的不同组合重现该问题:

g++-5 -m32 floating.cpp -O1
g++-8 -mfpmath=387 floating.cpp -O1

我在Windows或Cygwin上使用VC ++ / gcc / tcc尝试了几种组合,但该错误从未出现。我想生成的指令顺序是不一样的。

最后,请注意,可以使用一种特殊的方法来防止4.45或4.55出现此问题_Decimal32/64/128,但是真正缺乏支持...我花了很多时间才能够使用printf libdfp


0

就个人而言,我遇到了同样的问题-从gcc到VS。在大多数情况下,我认为最好避免优化。唯一值得的是在处理涉及大型浮点数据数组的数值方法时。即使在拆卸后,编译器的选择经常使我不知所措。通常,使用编译器内部函数或自己编写程序集会更容易。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.