GCC가 a * a * a * a * a * a를 (a * a * a) * (a * a * a)로 최적화하지 않는 이유는 무엇입니까?


2120

과학 응용 프로그램에서 수치 최적화를하고 있습니다. 내가 주목 한 것은 GCC가 호출 pow(a,2)을 컴파일 하여 호출 을 최적화 a*a하지만 호출 pow(a,6)이 최적화되지 않고 실제로 라이브러리 함수를 호출 pow하여 성능이 크게 저하 된다는 것 입니다. 반대로, 실행 가능한 Intel C ++ Compilericc 는 라이브러리 호출을 제거합니다 pow(a,6).

궁금한 점은 GCC 4.5.1 및 옵션 " " pow(a,6)a*a*a*a*a*a사용하여 대체 할 때 -O3 -lm -funroll-loops -msse45 개의 mulsd명령어를 사용한다는 것입니다 .

movapd  %xmm14, %xmm13
mulsd   %xmm14, %xmm13
mulsd   %xmm14, %xmm13
mulsd   %xmm14, %xmm13
mulsd   %xmm14, %xmm13
mulsd   %xmm14, %xmm13

내가 쓰는 경우 동안 (a*a*a)*(a*a*a), 그것은 생산합니다

movapd  %xmm14, %xmm13
mulsd   %xmm14, %xmm13
mulsd   %xmm14, %xmm13
mulsd   %xmm13, %xmm13

곱하기 명령어의 수를 3으로 줄 icc입니다. 비슷한 동작을합니다.

컴파일러가이 최적화 트릭을 인식하지 못하는 이유는 무엇입니까?


13
"pow (a, 6) 인식"은 무엇을 의미합니까?
Varun Madiath

659
음 ... 당신은 a a a a a (a a a) * (a a * a)가 부동 소수점 숫자와 같지 않다는 것을 알고 있습니까? -funsafe-math 또는 -ffast-math 등을 사용해야합니다.
데이먼

106
: 나는 당신이 "부동 소수점 연산에 대해 모든 컴퓨터 과학자한다 알고 무엇"데이비드 골드버그 읽을 제안 download.oracle.com/docs/cd/E19957-01/806-3568/...이 있는 후보다 완전한 이해를해야합니다 방금 들어간 타르 구덩이!
Phil Armstrong

189
완벽하게 합리적인 질문입니다. 20 년 전 저는 같은 일반적인 질문을했고, 그 단일 병목 현상을 해결함으로써 Monte Carlo 시뮬레이션의 실행 시간을 21 시간에서 7 시간으로 줄였습니다. 내부 루프의 코드는 프로세스에서 13 조 회 실행되었지만 시뮬레이션을 야간에 수행했습니다. (아래 답변 참조)

23
아마도 (a*a)*(a*a)*(a*a)믹스에 던져 넣을 수도 있습니다. 곱셈 횟수는 같지만 아마도 더 정확할 것입니다.
Rok Kralj

답변:


2738

때문에 부동 소수점 수학 연관되지 않습니다 . 부동 소수점 곱셈에서 피연산자를 그룹화하는 방법은 답의 숫자 정확도에 영향을줍니다.

결과적으로, 대부분의 컴파일러는 응답이 동일하게 유지된다고 확신 할 수 없거나 수치 정확도에 신경 쓰지 않는다고 말하지 않는 한 부동 소수점 계산 순서를 매우 보수적으로 유지합니다. 예를 들면 : 옵션을 다시 연결 부동 소수점 연산에 GCC 수 있습니다 GCC의, 또는 심지어 속도에 대한 정확성을 더욱 적극적으로 트레이드 오프를 허용 옵션을 선택합니다.-fassociative-math-ffast-math


10
예. -ffast-math를 사용하면 그러한 최적화가 수행됩니다. 좋은 생각! 그러나 우리 코드는 속도보다 정확도가 높기 때문에 통과하지 않는 것이 좋습니다.
xis

19
IIRC C99를 사용하면 컴파일러에서 "안전하지 않은"FP 최적화를 수행 할 수 있지만 x87 이외의 다른 GCC는 IEEE 754를 따르려고 합리적으로 시도합니다. "오류 범위"가 아닙니다. 정답은 하나뿐입니다 .
tc.

14
구현 세부 사항은 pow여기도 없습니다. 이 답변은 참조조차하지 않습니다 pow.
Stephen Canon

14
@nedR : ICC는 기본적으로 재 연결을 허용합니다. 표준 준수 동작을 얻으려면 -fp-model preciseICC 로 설정해야합니다 . clang그리고 gcc엄격한 적합성 wrt 재 연관으로 기본 설정됩니다.
Stephen Canon

49
@xis, 실제로는 -fassociative-math정확 하지 않습니다 . 그것은 단지의 a*a*a*a*a*a(a*a*a)*(a*a*a)다르다. 정확성에 관한 것이 아닙니다. 그것은 표준 적합성과 엄격히 반복 가능한 결과, 예를 들어 모든 컴파일러에서 동일한 결과에 관한 것입니다. 부동 소수점 숫자는 이미 정확하지 않습니다. 로 컴파일하는 것은 거의 부적절합니다 -fassociative-math.
폴 드레이퍼

652

Lambdageek는 제대로 연관성은 부동 소수점 번호를 보유하지 않기 때문에,의 "최적화"라는 지적a*a*a*a*a*a에이(a*a*a)*(a*a*a)값을 변경할 수 있습니다. 이것이 C99에서 허용하지 않는 이유입니다 (컴파일러 플래그 또는 pragma를 통해 사용자가 특별히 허용하지 않는 한). 일반적으로 프로그래머는 자신이 한 이유로 자신이 한 일을 썼다는 것을 전제로하며, 컴파일러는이를 존중해야합니다. 원하는(a*a*a)*(a*a*a)경우 쓰십시오.

그래도 글쓰기가 어려울 수 있습니다. 왜 컴파일러가 당신이 사용할 때 옳은 일을 할 수는 pow(a,6)없습니까? 그렇게하는 것이 잘못 되기 때문입니다 . 수학 라이브러리가 좋은 플랫폼에서는 또는 pow(a,6)보다 훨씬 더 정확합니다 . 일부 데이터를 제공하기 위해 Mac Pro에서 작은 실험을 수행하여 [1,2) 사이의 모든 단 정밀도 부동 숫자에 대해 a ^ 6을 평가할 때 최악의 오류를 측정했습니다.a*a*a*a*a*a(a*a*a)*(a*a*a)

worst relative error using    powf(a, 6.f): 5.96e-08
worst relative error using (a*a*a)*(a*a*a): 2.94e-07
worst relative error using     a*a*a*a*a*a: 2.58e-07

pow곱셈 트리 대신을 사용 하면 오차 4 의 오차 한계가 줄어 듭니다 . 컴파일러는 사용자가 라이센스를 부여하지 않은 경우 (예 :를 통해 -ffast-math) 오류를 증가시키는 "최적화"를하지 않아야합니다 (일반적으로 ).

GCC는 __builtin_powi(x,n)에 대한 대안으로 pow( )인라인 곱셈 트리를 생성해야합니다. 성능의 정확성을 떨어 뜨리고 싶지만 빠른 계산을 사용하지 않으려는 경우에 사용하십시오.


29
또한 Visual C ++는 '향상된'버전의 pow ()를 제공합니다. 호출하여 _set_SSE2_enable(<flag>)함께 flag=1가능하다면, 그것은 SSE2를 사용합니다. 이렇게하면 정확도가 약간 떨어지지 만 속도가 향상됩니다 (일부 경우). MSDN : _set_SSE2_enable ()pow ()
TkTech

18
@TkTech : 정확도의 감소는 사용 된 레지스터의 크기가 아니라 Microsoft의 구현 때문입니다. 라이브러리 라이터가 동기 부여를 받으면 32 비트 레지스터 만 사용하여 올림 pow올바르게 전달할 수 있습니다 . 대부분의 x87 기반 구현 보다 정확한 SSE 기반 pow구현이 있으며 속도와 약간의 정확도를 상쇄하는 구현도 있습니다.
Stephen Canon

9
@TkTech : 물론, 정확성의 감소는 SSE의 사용에 본질적인 것이 아니라 라이브러리 작성자의 선택에 의한 것임을 분명히하고 싶었습니다.
Stephen Canon

7
상대 오류를 계산하기 위해 여기에서 "골드 표준"으로 무엇을 사용했는지 알고 싶습니다. 일반적으로 그럴 것으로 예상 a*a*a*a*a*a했지만 실제로는 그렇지 않습니다! :)
j_random_hacker

8
@j_random_hacker : 단 정밀도 결과를 비교 한 이후로, 금 정도에 대해 배정 밀도이면 충분합니다. a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a single-precision calculations의 오류 보다 훨씬 작습니다.
Stephen Canon

168

또 다른 유사한 경우 대부분의 컴파일러하지 않습니다 최적화 a + b + c + d(a + b) + (c + d)(즉,로 주어로하고 평가 (이 두 번째 표현이 더 나은 파이프 라인 될 수 있기 때문에 최적화가) (((a + b) + c) + d)). 이것은 코너 케이스 때문입니다.

float a = 1e35, b = 1e-5, c = -1e35, d = 1e-5;
printf("%e %e\n", a + b + c + d, (a + b) + (c + d));

이 출력 1.000000e-05 0.000000e+00


10
이것은 정확히 동일하지 않습니다. 곱셈 / 나눗셈의 차인 순서 (0으로 나누기 제외)는 합계 / 빼기의 차인 순서보다 안전합니다. 겸손한 의견으로는, 컴파일러는 mults / div를 연관 시키려고 노력해야합니다. 그렇게하면 총 작업 수가 줄어들고 성능 게인 외에 정밀 게인도 있기 때문입니다.
CoffeDeveloper

4
@DarioOO : 더 안전하지 않습니다. 곱셈과 나눗셈은 지수의 덧셈과 뺄셈과 동일하며 순서를 변경하면 일시적으로 지수의 가능한 범위를 초과 할 수 있습니다. (지수는 정밀도의 손실을 겪지 않기 때문에 정확하게 동일하지는 않지만 ... 표현은 여전히 ​​제한적이며 재정렬은 표현할 수없는 값을 초래할 수 있습니다)
Ben Voigt

8
미적분학 배경이 빠진 것 같습니다. 2 개의 숫자를 곱하고 나누면 같은 양의 오류가 발생합니다. 빼기 / 더하기 2 숫자는 특히 2 숫자의 크기가 다른 경우 더 큰 오류를 유발할 수 있으므로 최종 오류의 작은 변화를 가져 오기 때문에 sub / add보다 mul / divide를 다시 정렬하는 것이 더 안전합니다.
CoffeDeveloper

8
@DarioOO : 위험은 mul / div와 다릅니다 : 재정렬은 최종 결과에서 무시할만한 변화를 만들거나 일정 시점 (이전에는 없었던 곳)에서 지수 오버플로가 발생하고 결과는 크게 다릅니다 (잠재적으로 + inf 또는 0).
Peter Cordes

@GameDeveloper 예측할 수없는 방식으로 정밀한 게인을 얻는 것은 큰 문제입니다.
curiousguy

80

Fortran (과학 컴퓨팅 용으로 설계됨)에는 전원 연산자가 내장되어 있으며, 내가 아는 한 Fortran 컴파일러는 일반적으로 설명하는 것과 비슷한 방식으로 정수 전력을 올릴 수 있도록 최적화합니다. 불행히도 C / C ++에는 파워 연산자가없고 라이브러리 함수 만 있습니다 pow(). 이것은 스마트 컴파일러가 pow특수한 경우를 위해 특별하게 처리 하고 더 빠른 방식으로 계산 하는 것을 방해하지는 않지만 덜 일반적으로 사용되는 것 같습니다 ...

몇 년 전에 나는 정수 전력을 최적의 방법으로 계산하는 것이 더 편리하도록 노력하고 있었고 다음을 생각해 냈습니다. 그것은 C가 아니라 C ++이며 여전히 최적화 / 인라인 방법에 대해 다소 똑똑한 컴파일러에 달려 있습니다. 어쨌든, 실제로 유용하게 사용될 수 있기를 바랍니다.

template<unsigned N> struct power_impl;

template<unsigned N> struct power_impl {
    template<typename T>
    static T calc(const T &x) {
        if (N%2 == 0)
            return power_impl<N/2>::calc(x*x);
        else if (N%3 == 0)
            return power_impl<N/3>::calc(x*x*x);
        return power_impl<N-1>::calc(x)*x;
    }
};

template<> struct power_impl<0> {
    template<typename T>
    static T calc(const T &) { return 1; }
};

template<unsigned N, typename T>
inline T power(const T &x) {
    return power_impl<N>::calc(x);
}

궁금한 점에 대한 설명 : 이것은 전력을 계산하는 최적의 방법을 찾지 못하지만 최적의 솔루션을 찾는 것은 NP- 완전한 문제 이므로 (어떻게 사용하는 것과 달리) 작은 전력에 대해서만 가치가 pow있기 때문에 소란 할 이유가 없습니다. 세부 사항으로.

그런 다음로 사용하십시오 power<6>(a).

이렇게하면 힘을 쉽게 입력 할 수 있고 (파 a렌스로 6 초 를 철자 할 필요가 없음 ), 보상 합산-ffast-math 과 같은 정밀한 의존성이있는 경우 (작업 순서가 필수적인 예) 없이 이러한 종류의 최적화를 수행 할 수 있습니다. .

아마도 이것이 C ++임을 잊어 버릴 수 있으며 C 프로그램에서 사용하십시오 (C ++ 컴파일러로 컴파일하는 경우).

이것이 유용 할 수 있기를 바랍니다.

편집하다:

이것이 내 컴파일러에서 얻는 것입니다.

를 들어 a*a*a*a*a*a,

    movapd  %xmm1, %xmm0
    mulsd   %xmm1, %xmm0
    mulsd   %xmm1, %xmm0
    mulsd   %xmm1, %xmm0
    mulsd   %xmm1, %xmm0
    mulsd   %xmm1, %xmm0

를 들어 (a*a*a)*(a*a*a),

    movapd  %xmm1, %xmm0
    mulsd   %xmm1, %xmm0
    mulsd   %xmm1, %xmm0
    mulsd   %xmm0, %xmm0

를 들어 power<6>(a),

    mulsd   %xmm0, %xmm0
    movapd  %xmm0, %xmm1
    mulsd   %xmm0, %xmm1
    mulsd   %xmm0, %xmm1

36
최적의 전력 트리를 찾는 것은 어려울 수 있지만 작은 전력에만 관심이 있기 때문에 명백한 대답은 한 번 사전 계산하고 (Knuth는 최대 100 개의 테이블을 제공함) 하드 코딩 된 테이블을 사용하는 것입니다 (gcc가 powi를 위해 내부적으로 수행함). .
Marc Glisse

7
최신 프로세서에서는 속도가 대기 시간에 의해 제한됩니다. 예를 들어, 곱셈 결과는 5주기 후에 사용 가능할 수 있습니다. 이러한 상황에서 전력을 생성하는 가장 빠른 방법을 찾는 것이 더 까다로울 수 있습니다.
gnasher729

3
상대 반올림 오류의 최저 상한 또는 최저 평균 상대 반올림 오류를 제공하는 전력 트리를 찾아 볼 수도 있습니다.
gnasher729

1
Boost도이를 지원합니다. 예 : boost :: math :: pow6 (n); 공통 요소를 추출하여 곱셈 횟수를 줄이려고한다고 생각합니다.
gast128

마지막 것은 (a ** 2) ** 3
minmaxavg와 같습니다.

62

GCC는 실제로 a가 정수일 때 최적화 a*a*a*a*a*a합니다 (a*a*a)*(a*a*a). 나는이 명령으로 시도했다 :

$ echo 'int f(int x) { return x*x*x*x*x*x; }' | gcc -o - -O2 -S -masm=intel -x c -

gcc 플래그는 많지만 멋진 것은 없습니다. 그들은 의미한다 : stdin에서 읽는다; O2 최적화 수준을 사용하십시오. 이진 대신 출력 어셈블리 언어 목록; 리스팅은 인텔 어셈블리 언어 구문을 사용해야합니다. 입력은 C 언어입니다 (일반적으로 언어는 입력 파일 확장자에서 유추되지만 stdin에서 읽을 때 파일 확장자는 없습니다). 그리고 stdout에 씁니다.

출력의 중요한 부분은 다음과 같습니다. 어셈블리 언어로 무슨 일이 일어나고 있는지 나타내는 몇 가지 주석으로 주석을 달았습니다.

; x is in edi to begin with.  eax will be used as a temporary register.
mov  eax, edi  ; temp = x
imul eax, edi  ; temp = x * temp
imul eax, edi  ; temp = x * temp
imul eax, eax  ; temp = temp * temp

우분투 파생물 인 Linux Mint 16 Petra에서 시스템 GCC를 사용하고 있습니다. gcc 버전은 다음과 같습니다.

$ gcc --version
gcc (Ubuntu/Linaro 4.8.1-10ubuntu9) 4.8.1

다른 포스터에서 언급했듯이 부동 소수점 산술은 연관성이 없으므로 부동 소수점에서는이 옵션을 사용할 수 없습니다.


12
2의 보수 오버플로가 정의되지 않은 동작이므로 정수 곱셈에 유효합니다. 오버플로가 발생하면 재정렬 작업에 관계없이 어딘가에서 발생합니다. 따라서 오버플로가없는 표현식은 동일하게 평가되고 오버플로가있는 표현식은 정의되지 않은 동작이므로 컴파일러가 오버플로가 발생하는 지점을 변경해도됩니다. gcc도이 작업을 수행합니다 unsigned int.
Peter Cordes

51

32 비트 부동 소수점 숫자 (예 : 1.024)는 1.024가 아니기 때문입니다. 컴퓨터에서 1.024는 (1.024-e)에서 (1.024 + e)까지의 간격입니다. 여기서 "e"는 오류를 나타냅니다. 어떤 사람들은 이것을 깨닫지 못하고 또한 *에서 *는 임의의 정밀도 숫자의 곱셈을 의미하며 그 숫자에 오류가 없음을 믿습니다. 일부 사람들이 이것을 깨닫지 못하는 이유는 아마도 초등학교에서 연습 한 수학 계산 일 것입니다. 오류가없는 이상적인 숫자로만 작업하고 곱셈을 수행하는 동안 단순히 "e"를 무시해도된다고 믿기 때문입니다. "float a = 1.2", "a * a * a"및 유사한 C 코드에 "e"가 암시되어 있지 않습니다.

대부분의 프로그래머가 C 표현식 a * a * a * a * a * a가 실제로 이상적인 숫자와 함께 작동하지 않는다는 생각을 인식하고 실행할 수 있다면 GCC 컴파일러는 "a * a를 최적화 할 수 있습니다. * a * a * a * a "는"t = (a * a); t * t * t "로 말하면 더 적은 수의 곱셈이 필요합니다. 그러나 불행히도 GCC 컴파일러는 코드를 작성하는 프로그래머가 "a"가 오류가 있거나없는 숫자라고 생각하는지 여부를 알지 못합니다. 따라서 GCC는 소스 코드의 모양 만 수행합니다. 왜냐하면 그것이 "네이 키드 아이"로 GCC에 표시되기 때문입니다.

당신이 어떤 프로그래머 알고 나면 ... 당신은 , 당신은 GCC를 말할 수있는 "-ffast - 수학"스위치를 사용할 수있다 "이봐, GCC, 나는 내가 뭐하는 거지 알아!". 이를 통해 GCC는 a * a * a * a * a * a를 다른 텍스트 조각으로 변환 할 수 있습니다. a * a * a * a * a * a와는 다르게 보이지만 여전히 오류 간격 내에서 숫자를 계산합니다. a * a * a * a * a * a. 이상적인 숫자가 아닌 간격으로 작업하고 있다는 것을 이미 알고 있으므로 괜찮습니다.


52
부동 소수점 숫자는 정확합니다. 그것들이 반드시 정확히 예상 한 것은 아닙니다. 또한 실제 예상 오류는 가수의 척도에 비례합니다. 즉, 일반적으로 최대 약 1 LSB이지만 엡실론을 사용한 기술 자체는 실제로 사물을 다루는 방법에 대한 근사치입니다. 조심하지 않으면 모든 작업을 수행하므로 부동 소수점에 사소한 작업을 수행하기 전에 수치 분석가에게 문의하십시오. 가능하면 적절한 라이브러리를 사용하십시오.
Donal Fellows

3
@DonalFellows : IEEE 표준에서는 부동 소수점 계산에서 소스 피연산자가 정확한 값인 경우 결과와 가장 정확하게 일치하는 결과를 산출해야하지만 이것이 실제로 정확한 값을 나타내는 것은 아닙니다 . 0.1f를 (1,677,722 +/- 0.5) / 16,777,216으로 간주하는 것이 많은 경우에 더 도움이됩니다.이 수치는 정확한 양 (1,677,722 +/- 0.5) / 16,777,216 (24 진수로 표시되어야 함).
supercat

23
@supercat : IEEE-754는 부동 소수점 데이터 정확한 값을 나타냅니다. 3.2-3.4 절이 관련 섹션입니다. 물론 3 +/- 0.5의 int x = 3의미 로 해석하도록 선택할 수있는 것처럼 다르게 해석하도록 선택할 수 있습니다 x.
Stephen Canon

7
@ supercat : 전적으로 동의하지만, Distance그것이 숫자 값과 정확히 일치 하지는 않습니다 . 이는 수치가 모델링되는 일부 물리량에 대한 근사치 일뿐임을 의미합니다.
Stephen Canon

10
수치 분석의 경우, 부동 소수점 숫자를 구간이 아니라 정확한 값 (원하는 값이 아닌 정확한 값)으로 해석하면 두뇌가 감사하게 생각합니다. 예를 들어, x가 0.1 미만의 오차가있는 4.5의 라운드이고 (x + 1)-x를 계산하는 경우 "간격"해석은 0.8에서 1.2 사이의 간격을두고 "정확한 값"해석은 결과는 배정 밀도에서 최대 2 ^ (-50)의 오차로 1이됩니다.
gnasher729

34

플로팅 표현식의 수축에 대해서는 아직 언급 한 포스터가 없습니다 (ISO C 표준, 6.5p8 및 7.12.2). 는 IF FP_CONTRACT그마로 설정되어 ON, 컴파일러는 같은 식 간주시킨다 a*a*a*a*a*a번의 라운딩 정확하게 평가하는 것처럼, 하나의 동작으로서. 예를 들어, 컴파일러는 더 빠르고 정확한 내부 전력 함수로이를 대체 할 수 있습니다. 이는 최종 사용자가 제공 한 컴파일러 옵션이 때때로 잘못 사용될 수 있지만 동작은 소스 코드에서 프로그래머가 직접 동작을 부분적으로 제어하기 때문에 특히 흥미 롭습니다.

FP_CONTRACTpragma 의 기본 상태 는 구현 정의이므로 컴파일러는 기본적으로 이러한 최적화를 수행 할 수 있습니다. 따라서 IEEE 754 규칙을 엄격하게 준수해야하는 이식 가능한 코드는 명시 적으로로 설정해야합니다 OFF.

컴파일러가이 pragma를 지원하지 않는 경우 개발자가로 설정하도록 선택한 경우 이러한 최적화를 피함으로써 보수적이어야합니다 OFF.

GCC는이 pragma를 지원하지 않지만 기본 옵션을 사용하면이 pragma를 가정합니다 ON. 따라서 하드웨어 FMA가있는 대상의 경우 a*b+cfma (a, b, c) 로의 변환을 방지하려면 -ffp-contract=off(pragma를 명시 적으로로 설정 OFF) 또는 -std=c99(GCC에게 일부를 준수하도록 지시 하는) 옵션을 제공 해야합니다. C 표준 버전, 여기서 C99는 위 단락을 따릅니다). 과거에는 후자의 옵션이 변환을 방해하지 않았으므로 GCC 가이 시점에서 준수하지 않았 음을 의미합니다. https://gcc.gnu.org/bugzilla/show_bug.cgi?id=37845


3
오래 지속되는 인기 질문은 때때로 나이를 보여줍니다. 이 질문은 GCC가 당시의 C99 표준을 정확히 준수하지 않아서 면제 될 수있는 2011 년에 요청 및 답변되었습니다. 물론 지금은 2014 년이므로 GCC…
Pascal Cuoq 2016 년

그래도 허용되는 대답없이 비교적 최근의 부동 소수점 질문에 대답해서는 안됩니까? 기침 stackoverflow.com/questions/23703408 기침
Pascal Cuoq

나는 gcc가 C99 부동 소수점 pragma를 구현하지 않는다고 방해했다.
David Monniaux

1
@DavidMonniaux pragma는 정의에 따라 구현할 수있는 옵션입니다.
Tim Seguine

2
@TimSeguine 그러나 pragma가 구현되지 않은 경우 기본값은 구현에 가장 제한적이어야합니다. 나는 그것이 다윗이 생각한 것이라고 생각합니다. GCC 에서는 ISO C 모드를 사용하는 경우 FP_CONTRACT에 대해 수정되었습니다 . 여전히 pragma를 구현하지 않지만 ISO C 모드에서는 pragma가 꺼져 있다고 가정합니다.
vinc17

28

Lambdageek이 지적했듯이 float 곱셈은 연관성이 없으며 정확도가 떨어질 수 있지만 정확도가 높아지면 결정 론적 응용 프로그램을 원하기 때문에 최적화에 반대 할 수 있습니다. 예를 들어 게임 시뮬레이션 클라이언트 / 서버에서 모든 클라이언트는 부동 소수점 계산을 결정하기 원하는 동일한 세계를 시뮬레이션해야합니다.


3
@greggo 아니요, 여전히 결정 론적입니다. 단어의 의미에 임의성이 추가되지 않습니다.
Alice

9
@Alice Bjorn은 다른 플랫폼과 다른 컴파일러 버전 등 (프로그래머가 제어 할 수없는 외부 변수)에서 동일한 결과를주는 코드의 의미에서 '결정 론적'을 사용하고 있음이 분명합니다. 런타임시 실제 숫자 임의성 이것이 이것이 단어의 올바른 사용이 아니라고 지적한다면, 나는 그것에 대해 논쟁하지 않을 것입니다.
greggo

5
@greggo 그가 말한 것을 해석하는 것 외에는 여전히 잘못입니다. 이는 플랫폼 전체에서 (모두는 아니지만) 대부분의 작업에 동일한 특성을 제공하기 위해 IEEE 754의 전체 요점입니다. 이제 그는 플랫폼이나 컴파일러 버전에 대해 언급하지 않았으며 모든 원격 서버 / 클라이언트의 모든 단일 작업을 동일하게 유지하려는 경우 유효한 관심사가 될 것입니다. 더 나은 단어는 "신뢰할만한 유사"또는 무언가 일 수 있습니다.
Alice

8
@Alice 당신은 의미를 주장함으로써 당신 자신을 포함한 모든 사람의 시간을 낭비하고 있습니다. 그의 의미는 분명했다.
Lanaru

11
@Lanaru 표준의 요점은 의미론입니다. 그의 의미는 분명하지 않았다.
Alice

28

"pow"와 같은 라이브러리 함수는 일반적으로 가능한 최소 오류 (일반적인 경우)를 생성하기 위해 신중하게 제작됩니다. 이것은 일반적으로 스플라인으로 함수를 근사화합니다 (Pascal의 의견에 따르면 가장 일반적인 구현은 Remez 알고리즘을 사용하는 것 같습니다 )

기본적으로 다음 작업 :

pow(x,y);

단일 곱셈 또는 나눗셈의 오차와 대략 같은 크기 의 고유 오차가 있습니다.

다음 작업 중 :

float a=someValue;
float b=a*a*a*a*a*a;

5 곱셈을 결합하기 때문에 단일 곱셈 또는 나눗셈 오류의 5 배 이상인 고유 오류가 있습니다.

컴파일러는 수행하는 최적화 종류에주의해야합니다.

  1. 최적화하는 경우 pow(a,6)a*a*a*a*a*a그것을하는 성능을 향상 있지만, 크게 부동 소수점 숫자의 정확성을 줄일 수 있습니다.
  2. "a"가 실수없이 곱셈을 허용하는 특별한 값 (2의 거듭 제곱 또는 작은 정수)이기 때문에 최적화 a*a*a*a*a*a 하면 pow(a,6)실제로 정확도가 떨어질 수 있습니다.
  3. 최적화하는 경우 pow(a,6)(a*a*a)*(a*a*a)또는 (a*a)*(a*a)*(a*a)여전히 존재하기에 비해 정확성의 손실 될 수 있습니다 pow기능.

일반적으로 임의 부동 소수점 값의 경우 "pow"는 궁극적으로 작성할 수있는 함수보다 정확도가 높지만 일부 특수한 경우 다중 곱셈의 정확도와 성능이 더 우수 할 수 있습니다. 결국 아무도 코드를 "최적화"하지 않도록 코드에 주석을 답니다.

최적화하기 위해 (개인의 의견, 그리고 특정 최적화 또는 컴파일러 플래그를 제외하고 GCC에서 분명히 선택) 유일한 것은 "pow (a, 2)"를 "a * a"로 바꾸는 것입니다. 이것이 컴파일러 벤더가해야 할 유일한 제정신 일입니다.


7
downvoters는이 답변이 완벽하다는 것을 알고 있어야합니다. 나는 내 대답을 뒷받침하기 위해 수십 개의 출처와 문서를 인용 할 수 있으며 아마도 downvoter보다 부동 소수점 정밀도에 더 관련되어 있습니다. 다른 답변에서 다루지 않는 누락 된 정보를 추가하는 StackOverflow에서는 완벽하게 합리적이므로 정중하고 이유를 설명하십시오.
CoffeDeveloper 2016 년

1
Stephen Canon의 대답은 당신이해야 할 것을 다루는 것 같습니다. libms는 스플라인으로 구현되어야한다고 주장하는 것 같습니다. 더 일반적으로 인수 축소 (구현되는 함수에 따라 다름)와 Remez 알고리즘의 다소 복잡한 변형에 의해 얻은 계수의 단일 다항식을 사용합니다. 접합점에서의 부드러움은 libm 함수를 추구 할 가치가있는 목표로 간주되지 않습니다.
Pascal Cuoq

답의 후반부에서는 컴파일러가 소스 코드의 말을 구현하는 코드를 생성해야한다는 점을 완전히 놓칩니다. 또한 "정확도"를 의미 할 때 "정밀도"라는 단어를 사용합니다.
Pascal Cuoq

의견을 보내 주셔서 감사합니다. 답변을 약간 수정했습니다. 마지막 2 줄에 새로운 내용이 여전히 있습니다 ^^
CoffeDeveloper

27

이 사례가 전혀 최적화되지 않았을 것입니다. 표현식에 전체 연산을 제거하기 위해 다시 그룹화 할 수있는 하위 표현식이 포함 된 경우가 종종 있습니다. 필자는 컴파일러 작성자가 거의 발생하지 않는 엣지 케이스를 다루지 않고 눈에 띄게 개선 될 수있는 영역에 시간을 투자 할 것으로 기대합니다.

이 답변이 실제로 적절한 컴파일러 스위치로 최적화 될 수 있다는 다른 답변에서 배우는 것에 놀랐습니다. 최적화가 사소한 것이거나 훨씬 일반적인 최적화의 경우이거나 컴파일러 작성자가 매우 철저했습니다.

여기에서 한 것처럼 컴파일러에 힌트를 제공하는 데 아무런 문제가 없습니다. 명령문과 표현식을 재 배열하여 차이점이 무엇인지 확인하는 것이 마이크로 최적화 프로세스의 정상적이고 예상되는 부분입니다.

일관된 결과없이 일관된 결과를 제공하기 위해 두 표현식을 고려할 때 컴파일러가 정당화 될 수 있지만, 이러한 제한에 구속 될 필요는 없습니다. 그 차이는 엄청나게 작을 것입니다. 그래서 그 차이가 당신에게 중요하다면, 표준 부동 소수점 산술을 사용해서는 안됩니다.


17
다른 주석가가 지적했듯이, 이것은 터무니없는 점에서는 사실이 아닙니다. 그 차이는 비용의 절반에서 10 % 정도가 될 수 있으며, 타이트한 루프로 실행되는 경우, 많은 양의 추가 정밀도를 얻기 위해 많은 명령을 낭비하게됩니다. 몬테카를로를 할 때 표준 FP를 사용하지 말아야한다는 것은 항상 비행기를 사용하여 국가를 가로 질러야한다는 것과 같습니다. 그것은 많은 외부 성을 무시합니다. 마지막으로 이것은 드문 최적화가 아닙니다. 데드 코드 분석 및 코드 축소 / 리 팩터는 매우 일반적입니다.
Alice

21

이 질문에 대한 몇 가지 좋은 답변이 있지만 완전성을 기하기 위해 C 표준의 해당 섹션이 5.1.2.2.3 / 15 (1.9 / 9의 섹션 1.9 / 9와 동일 함)를 지적하고 싶었습니다. C ++ 11 표준). 이 섹션에서는 연산자가 실제로 연관되거나 정식 인 경우에만 연산자를 다시 그룹화 할 수 있다고 설명합니다.


12

gcc는 실제로 부동 소수점 숫자에도이 최적화를 수행 할 수 있습니다. 예를 들어

double foo(double a) {
  return a*a*a*a*a*a;
}

된다

foo(double):
    mulsd   %xmm0, %xmm0
    movapd  %xmm0, %xmm1
    mulsd   %xmm0, %xmm1
    mulsd   %xmm1, %xmm0
    ret

-O -funsafe-math-optimizations. 이 재정렬은 IEEE-754를 위반하므로 플래그가 필요합니다.

Peter Cordes가 주석에서 지적했듯이 부호있는 정수 -funsafe-math-optimizations는 오버플로가 없을 때 정확하게 보유하고 오버플로가있는 경우 정의되지 않은 동작을 얻으 므로이 최적화를 수행 할 수 있습니다. 그래서 당신은 얻을

foo(long):
    movq    %rdi, %rax
    imulq   %rdi, %rax
    imulq   %rdi, %rax
    imulq   %rax, %rax
    ret

그냥 -O. 부호없는 정수의 경우 2의 모드 거듭 제곱으로 작동하기 때문에 훨씬 쉬우므로 오버플로가 발생하더라도 자유롭게 재정렬 할 수 있습니다.


1
Godbolt 는 double, int 및 unsigned와 연결 됩니다. GCC 모두 최적화를 연타 세 (와 같은 방식 -ffast-math)
피터 코르

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