Programing

최적화가 활성화 된 다른 부동 소수점 결과-컴파일러 버그?

crosscheck 2020. 8. 17. 08:00
반응형

최적화가 활성화 된 다른 부동 소수점 결과-컴파일러 버그?


아래 코드는 최적화 여부에 관계없이 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

그러나 최적화 된 g ++ ( O1- O3)는 다음을 출력합니다.

4.5
4.5

volatilet 앞에 키워드를 추가하면 작동하므로 최적화 버그가있을 수 있습니까?

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:fastVisual Studio 2008이 켜져 있는 이유에 관심이있는 사람 이 있습니까? Visual Studio 2008이 g ++보다이 문제에서 더 신뢰할 수있는 것 같습니다.


Intel x86 프로세서 double내부적으로 80 비트 확장 정밀도를 사용하지만 일반적으로 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.

출력은 다음과 같아야합니다. 4.5 4.6 무한 정밀도를 가졌거나 바이너리 기반 부동 소수점 표현이 아닌 10 진수 기반을 사용하는 장치로 작업하는 경우 출력이됩니다. 그러나 당신은 그렇지 않습니다. 대부분의 컴퓨터는 이진 IEEE 부동 소수점 표준을 사용합니다.

As Maxim Yegorushkin already noted in his answer, part of the problem is that internally your computer is using an 80 bit floating point representation. This is just part of the problem, though. The basis of the problem is that any number of the form n.nn5 does not have an exact binary floating representation. Those corner cases are always inexact numbers.

If you really want your rounding to be able to reliably round these corner cases, you need a rounding algorithm that addresses the fact that n.n5, n.nn5, or n.nnn5, etc. (but not n.5) is always inexact. Find the corner case that determines whether some input value rounds up or down and return the rounded-up or rounded-down value based on a comparison to this corner case. And you do need to take care that a optimizing compiler will not put that found corner case in an extended precision register.

See How does Excel successfully Rounds Floating numbers even though they are imprecise? for such an algorithm.

Or you can just live with the fact that the corner cases will sometimes round erroneously.


Different compilers have different optimization settings. Some of those faster optimization settings do not maintain strict floating-point rules according to IEEE 754. Visual Studio has a specific setting, /fp:strict, /fp:precise, /fp:fast, where /fp:fast violates the standard on what can be done. You might find that this flag is what controls the optimization in such settings. You may also find a similar setting in GCC which changes the behaviour.

If this is the case then the only thing that's different between the compilers is that GCC would look for the fastest floating point behaviour by default on higher optimisations, whereas Visual Studio does not change the floating point behaviour with higher optimization levels. Thus it might not necessarily be an actual bug, but intended behaviour of an option you didn't know you were turning on.


To those who can't reproduce the bug: do not uncomment the commented out debug stmts, they affect the result.

This implies that the problem is related to the debug statements. And it looks like there's a rounding error caused by loading the values into registers during the output statements, which is why others found that you can fix this with -ffloat-store

Further question:

I was wondering, should I always turn on -ffloat-store option?

To be flippant, there must be a reason that some programmers don't turn on -ffloat-store, otherwise the option wouldn't exist (likewise, there must be a reason that some programmers do turn on -ffloat-store). I wouldn't recommend always turning it on or always turning it off. Turning it on prevents some optimizations, but turning it off allows for the kind of behavior you're getting.

But, generally, there is some mismatch between binary floating point numbers (like the computer uses) and decimal floating point numbers (that people are familiar with), and that mismatch can cause similar behavior to what your getting (to be clear, the behavior you're getting is not caused by this mismatch, but similar behavior can be). The thing is, since you already have some vagueness when dealing with floating point, I can't say that -ffloat-store makes it any better or any worse.

Instead, you may want to look into other solutions to the problem you're trying to solve (unfortunately, Koenig doesn't point to the actual paper, and I can't really find an obvious "canonical" place for it, so I'll have to send you to Google).


If you're not rounding for output purposes, I would probably look at std::modf() (in cmath) and std::numeric_limits<double>::epsilon() (in limits). Thinking over the original round() function, I believe it would be cleaner to replace the call to std::floor(d + .5) with a call to this function:

// 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;
}

I think that suggests the following improvement:

// 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());
}

A simple note: std::numeric_limits<T>::epsilon() is defined as "the smallest number added to 1 that creates a number not equal to 1." You usually need to use a relative epsilon (i.e., scale epsilon somehow to account for the fact that you're working with numbers other than "1"). The sum of d, .5 and std::numeric_limits<double>::epsilon() should be near 1, so grouping that addition means that std::numeric_limits<double>::epsilon() will be about the right size for what we're doing. If anything, std::numeric_limits<double>::epsilon() will be too large (when the sum of all three is less than one) and may cause us to round some numbers up when we shouldn't.


Nowadays, you should consider std::nearbyint().


I digged more into this problem and I can bring more precisions. First, the exact representations of 4.45 and 4.55 according to gcc on x84_64 are the following (with libquadmath to print the last precision):

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

As Maxim said above, the problem is due to the 80 bits size of the FPU registers.

But why is the problem never occuring on Windows? on IA-32, the x87 FPU was configured to use an internal precision for the mantissa of 53 bits (equivalent to a total size of 64 bits: double). For Linux and Mac OS, the default precision of 64 bits was used (equivalent to a total size of 80 bits: long double). So the problem should be possible, or not, on these different platforms by changing the control word of the FPU (assuming the sequence of instructions would trigger the bug). The issue was reported to gcc as bug 323 (read at least the comment 92! ).

To show the mantissa precision on Windows, you can compile this in 32 bits with VC++:

#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]);
}

and on 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]);
}

Note that with gcc you can set the FPU precision with -mpc32/64/80, though it is ignored in Cygwin. But keep in mind that it will modify the size of the mantissa, but not the exponent one, letting the door open to other kinds of different behavior.

On x86_64 architecture, SSE is used as said by tmandry, so the problem will not occur unless you force the old x87 FPU for FP computing with -mfpmath=387, or unless you compile in 32 bits mode with -m32 (you will need multilib package). I could reproduce the problem on Linux with different combinations of flags and versions of gcc:

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

I tried a few combinations on Windows or Cygwin with VC++/gcc/tcc but the bug never showed up. I suppose the sequence of instruction generated is not the same.

Finally, note that an exotic way to prevent this problem with 4.45 or 4.55 would be to use _Decimal32/64/128, but support is really scarce... I spent a lot of time just to be able to do a printf with libdfp !


The accepted answer is correct if you are compiling to an x86 target that doesn't include SSE2. All modern x86 processors support SSE2, so if you can take advantage of it, you should:

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

Let's break this down.

-mfpmath=sse -msse2. This performs rounding by using SSE2 registers, which is much faster than storing every intermediate result to memory. Note that this is already the default on GCC for x86-64. From the GCC wiki:

On more modern x86 processors that support SSE2, specifying the compiler options -mfpmath=sse -msse2 ensures all float and double operations are performed in SSE registers and correctly rounded. These options do not affect the ABI and should therefore be used whenever possible for predictable numerical results.

-ffp-contract=off. Controlling rounding isn't enough for an exact match, however. FMA (fused multiply-add) instructions can change the rounding behavior versus its non-fused counterparts, so we need to disable it. This is the default on Clang, not GCC. As explained by this answer:

An FMA has only one rounding (it effectively keeps infinite precision for the internal temporary multiply result), while an ADD + MUL has two.

By disabling FMA, we get results that exactly match on debug and release, at the cost of some performance (and accuracy). We can still take advantage of other performance benefits of SSE and AVX.


Personally, I have hit the same problem going the other way - from gcc to VS. In most instances I think it is better to avoid optimisation. The only time it is worthwhile is when you're dealing with numerical methods involving large arrays of floating point data. Even after disassembling I'm often underwhelmed by the compilers choices. Very often it's just easier to use compiler intrinsics or just write the assembly yourself.

참고URL : https://stackoverflow.com/questions/7517588/different-floating-point-result-with-optimization-enabled-compiler-bug

반응형