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


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

그러나 최적화 된 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 ++보다이 문제에서 더 신뢰할 수있는 것 같습니다.


51
모든 새로운 SO 사용자에게 : 이것이 질문하는 방법입니다. +1
tenfour 2011 년

1
FWIW, MinGW를 사용하여 g ++ 4.5.0에서 올바른 출력을 얻고 있습니다.
Steve Blackwell

2
ideone은 4.3.4 사용 ideone.com/b8VXg
다니엘 A. 화이트

5
당신은 당신의 루틴이 모든 종류의 출력물과 함께 안정적으로 작동하지 않을 것이라는 점을 기억해야합니다. double을 정수로 반올림하는 것과는 달리 모든 실수를 표현할 수있는 것은 아니므로 이와 같은 버그가 더 많이 발생할 것으로 예상해야한다는 사실에 취약합니다.
Jakub Wieczorek

2
버그를 재현 할 수없는 분들께 : 주석 처리 된 디버그 stmts의 주석 처리를 제거하지 마십시오. 결과에 영향을 미칩니다.
n. '대명사'm.

답변:


91

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.

x86_64의에서 컴파일러가 SSE 레지스터를 사용하여 구축 floatdouble확장 된 정밀도를 사용하지 않는이 문제가 발생하지 않습니다 그래서, 기본적으로.

gcc컴파일러 옵션이이를-mfpmath 제어합니다.


20
이것이 답이라고 생각합니다. 상수 4.55는 64 비트에서 가장 가까운 이진 표현 인 4.54999999999999로 변환됩니다. 10을 곱하고 다시 64 비트로 반올림하면 45.5가됩니다. 반올림 단계를 80 비트 레지스터에 유지하여 건너 뛰면 45.4999999999999가됩니다.
Mark Ransom

감사합니다.이 옵션도 모르겠습니다. 그러나 나는 항상 -ffloat-store 옵션을 켜야합니까? 테스트 한 g ++ 버전은 CentOS / Redhat 5 및 CentOS / Redhat 6과 함께 제공되기 때문에 이러한 플랫폼에서 많은 프로그램을 컴파일했기 때문에 프로그램 내에서 예기치 않은 버그가 발생 할까 걱정됩니다.
Bear

5
@Bear, 디버그 문으로 인해 값이 레지스터에서 메모리로 플러시됩니다.
Mark Ransom

2
@Bear, 일반적으로 응용 프로그램은 확장 된 정밀도의 이점을 누릴 수 있습니다. 단, 64 비트 부동 소수점이 inf. 좋은 경험 법칙은 없으며 단위 테스트는 확실한 답을 줄 수 있습니다.
Maxim Egorushkin 2011 년

2
@bear 일반적으로 완벽하게 예측 가능한 결과가 필요하거나 인간이 종이에 합계를 계산하는 것과 정확히 일치하는 결과가 필요한 경우 부동 소수점을 피해야합니다. -ffloat-store는 예측 불가능한 원인 중 하나를 제거하지만 마법의 총알은 아닙니다.
plugwash

10

출력은 다음과 같아야합니다. 4.5 4.6 무한 정밀도를 가졌거나 바이너리 기반 부동 소수점 표현이 아닌 10 진수 기반을 사용하는 장치로 작업하는 경우 출력이됩니다. 그러나 당신은 그렇지 않습니다. 대부분의 컴퓨터는 이진 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-mathGCC 용 스위치 가 있는데 , -O인용 이후 최적화 수준 에 의해 켜지지 않습니다. "수학 함수에 대한 IEEE 또는 ISO 규칙 / 사양의 정확한 구현에 의존하는 프로그램에 대해 잘못된 출력이 발생할 수 있습니다."
Mat

@Mat : 나는 -ffast-math몇 가지 다른 것을 시도했지만 g++ 4.4.3여전히 문제를 재현 할 수 없습니다.
NPE 2011 년

니스 :와 -ffast-math내가 어떻게해야합니까 4.5보다 최적화 레벨에 대한 두 경우 모두에서 0.
Kerrek SB 2011

(수정 내가 얻을 4.5-O1하고 -O2,은 불가능 -O0하고 -O3GCC 4.4.3에서하지만,와 -O1,2,3GCC 4.6.1에서.)
Kerrek SB

4

버그를 재현 할 수없는 분들께 : 주석 처리 된 디버그 stmts의 주석 처리를 제거하지 마십시오. 결과에 영향을 미칩니다.

이는 문제가 디버그 문과 관련되어 있음을 의미합니다. 출력 문 중에 값을 레지스터에로드하여 반올림 오류가 발생한 것 같습니다.이 때문에 다른 사용자가이 문제를 해결할 수 있음을 알게되었습니다.-ffloat-store

추가 질문 :

항상 -ffloat-store옵션을 켜야 하나요?

어리석게 말하면 일부 프로그래머가 켜지지 않는 이유가 있어야합니다. -ffloat-store그렇지 않으면 옵션이 존재하지 않을 것입니다 (마찬가지로 일부 프로그래머 켜는 이유가 있어야합니다 -ffloat-store). 항상 전원을 켜거나 끄는 것을 권장하지 않습니다. 이 기능을 켜면 일부 최적화가 방지되지만이 기능을 끄면 얻을 수있는 종류의 동작이 허용됩니다.

그러나 일반적으로 컴퓨터에서 사용하는 것과 같은 이진 부동 소수점 숫자와 사람들이 익숙한 10 진수 부동 소수점 숫자 사이 에는 약간의 불일치 가 있으며 그 불일치는 당신이 얻는 것과 유사한 동작을 유발할 수 있습니다. 당신은되어지고있어 하지 이 불일치에 의해 발생하지만, 비슷한 동작이 가능 )합니다. 문제는 부동 소수점을 다룰 때 이미 약간의 모호함이 -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에 추가 된 가장 작은 숫자"로 정의됩니다. 일반적으로 상대 엡실론을 사용해야합니다 (예 : "1"이외의 숫자로 작업하고 있다는 사실을 설명하기 위해 엡실론 배율을 조정합니다). 의 합은 d, .5그리고 std::numeric_limits<double>::epsilon()그래서 또한 수단 그룹화, 1 가까워 야 std::numeric_limits<double>::epsilon()우리가 무슨 일을하는지에 적합한 크기에 대한 것입니다. 어떤 std::numeric_limits<double>::epsilon()것이 든 너무 커서 (3 개 모두의 합이 1보다 작을 때), 그러지 말아야 할 때 일부 숫자를 반올림 할 수 있습니다.


요즘에는 std::nearbyint().


"상대 입실론"은 1 ulp (마지막 자리에 1 단위)라고합니다. x - nextafter(x, INFINITY)x에 대한 1ulp와 관련이 있습니다 (하지만 사용하지 마십시오. 코너 케이스가 있다고 확신하며 방금 구성했습니다). 의 cppreference 예제 epsilon() 에는 ULP 기반 상대 오류를 얻기 위해 확장하는 예제가 있습니다.
Peter Cordes

2
BTW, 2016 년 답변 -ffloat-store은 : 처음부터 x87을 사용하지 마십시오. -mfpmath=sse -msse2SSE / SSE2에는 추가 정밀도가없는 임시 파일이 있으므로 SSE2 수학 (64 비트 바이너리 또는 딱딱한 오래된 32 비트 바이너리 만들기)을 사용하십시오. double그리고 float실제로 IEEE 64 비트 또는 32 비트 포맷이다 XMM 레지스터에 바르. (레지스터가 항상 80 비트이고 메모리에 저장하면 32 비트 또는 64 비트로 반올림되는 x87과는 다릅니다.)
Peter Cordes

3

SSE2를 포함하지 않는 x86 대상으로 컴파일하는 경우 허용되는 대답은 정확합니다. 모든 최신 x86 프로세서는 SSE2를 지원하므로이를 활용할 수 있다면 다음을 수행해야합니다.

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

이것을 분해합시다.

-mfpmath=sse -msse2. 이것은 모든 중간 결과를 메모리에 저장하는 것보다 훨씬 빠른 SSE2 레지스터를 사용하여 반올림을 수행합니다. 이것은 이미 x86-64 용 GCC 의 기본값 입니다. 로부터 GCC 위키 :

SSE2를 지원하는 최신 x86 프로세서에서 컴파일러 옵션 -mfpmath=sse -msse2을 지정 하면 모든 부동 및 이중 연산이 SSE 레지스터에서 수행되고 올바르게 반올림됩니다. 이러한 옵션은 ABI에 영향을주지 않으므로 가능한 한 예측 가능한 수치 결과를 위해 사용해야합니다.

-ffp-contract=off. 그러나 반올림을 제어하는 ​​것만으로는 정확히 일치하지 않습니다. FMA (fused multiply-add) 명령어는 반올림 동작을 융합되지 않은 상대와 비교하여 변경할 수 있으므로이를 비활성화해야합니다. GCC가 아닌 Clang의 기본값입니다. 이 답변에서 설명했듯이 :

FMA는 하나의 반올림 (내부 임시 곱셈 결과에 대한 무한 정밀도를 효과적으로 유지함) 만있는 반면 ADD + MUL에는 2 개가 있습니다.

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

마찬가지로 맥심 상술 상기 문제가 기인 FPU 레지스터의 80 비트 크기이다.

그러나 Windows에서 문제가 발생하지 않는 이유는 무엇입니까? IA-32에서 x87 FPU는 53 비트 가수에 대해 내부 정밀도를 사용하도록 구성되었습니다 (총 64 비트 크기에 해당 double). Linux 및 Mac OS의 경우 기본 정밀도 64 비트가 사용되었습니다 (총 크기 80 비트에 해당 long double). 따라서 FPU의 제어 단어를 변경하여 이러한 서로 다른 플랫폼에서 문제가 가능하거나 불가능해야합니다 (명령 시퀀스가 ​​버그를 유발한다고 가정). 이 문제는 gcc에 버그 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를 사용하면 -mpc32/64/80Cygwin에서는 무시 되지만를 사용하여 FPU 정밀도를 설정할 수 있습니다 . 그러나 가수의 크기는 수정되지만 지수는 수정되지 않아 다른 종류의 다른 동작에 대한 문을 열 수 있다는 점을 명심하십시오.

x86_64 아키텍처에서 SSE는 tmandry 에서 말한대로 사용 되므로 FP 컴퓨팅을 위해 이전 x87 FPU를으로 강제 실행 -mfpmath=387하거나 32 비트 모드로 컴파일하지 않는 한 문제가 발생하지 않습니다 -m32(multilib 패키지가 필요함). 다양한 플래그 조합과 gcc 버전을 사용하여 Linux에서 문제를 재현 할 수 있습니다.

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로 같은 문제를 겪었습니다. 대부분의 경우 최적화를 피하는 것이 좋습니다. 가치가있는 유일한 경우는 부동 소수점 데이터의 큰 배열을 포함하는 수치 방법을 다룰 때입니다. 분해 후에도 나는 종종 컴파일러의 선택에 압도 당합니다. 종종 컴파일러 내장 함수를 사용하거나 어셈블리를 직접 작성하는 것이 더 쉽습니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.