0.1f를 0으로 변경하면 성능이 10 배 저하되는 이유는 무엇입니까?


1527

이 코드가 왜

const float x[16] = {  1.1,   1.2,   1.3,     1.4,   1.5,   1.6,   1.7,   1.8,
                       1.9,   2.0,   2.1,     2.2,   2.3,   2.4,   2.5,   2.6};
const float z[16] = {1.123, 1.234, 1.345, 156.467, 1.578, 1.689, 1.790, 1.812,
                     1.923, 2.034, 2.145,   2.256, 2.367, 2.478, 2.589, 2.690};
float y[16];
for (int i = 0; i < 16; i++)
{
    y[i] = x[i];
}

for (int j = 0; j < 9000000; j++)
{
    for (int i = 0; i < 16; i++)
    {
        y[i] *= x[i];
        y[i] /= z[i];
        y[i] = y[i] + 0.1f; // <--
        y[i] = y[i] - 0.1f; // <--
    }
}

다음 비트보다 10 배 이상 빠르게 실행 (표시된 곳을 제외하고 동일)?

const float x[16] = {  1.1,   1.2,   1.3,     1.4,   1.5,   1.6,   1.7,   1.8,
                       1.9,   2.0,   2.1,     2.2,   2.3,   2.4,   2.5,   2.6};
const float z[16] = {1.123, 1.234, 1.345, 156.467, 1.578, 1.689, 1.790, 1.812,
                     1.923, 2.034, 2.145,   2.256, 2.367, 2.478, 2.589, 2.690};
float y[16];
for (int i = 0; i < 16; i++)
{
    y[i] = x[i];
}

for (int j = 0; j < 9000000; j++)
{
    for (int i = 0; i < 16; i++)
    {
        y[i] *= x[i];
        y[i] /= z[i];
        y[i] = y[i] + 0; // <--
        y[i] = y[i] - 0; // <--
    }
}

Visual Studio 2010 SP1로 컴파일 할 때 최적화 수준이었다 -02함께 sse2수있었습니다. 다른 컴파일러로는 테스트하지 않았습니다.


10
차이를 어떻게 측정 했습니까? 그리고 컴파일 할 때 어떤 옵션을 사용 했습니까?
James Kanze

158
이 경우 컴파일러가 왜 +/- 0을 삭제하지 않습니까?!
Michael Dorgan

127
@ Zyx2000 컴파일러는 바보 같은 곳이 아닙니다. 당신이 사용 여부를 동일한 코드를 뱉어 것을 LINQPad 쇼에서 사소한 예를 분해 0, 0f, 0d, 또는 (int)0문맥에서이 곳 double이 필요하다.
밀리 모오스

14
최적화 수준은 무엇입니까?
Otto Allmendinger

답변:


1616

비정규 화 된 부동 소수점 의 세계에 오신 것을 환영합니다 ! 그들은 성능에 혼란을 줄 수 있습니다!

비정규 (또는 비정규) 숫자는 부동 소수점 표현에서 0에 매우 가까운 일부 추가 값을 얻기위한 일종의 해킹입니다. 비정규 부동 소수점에 대한 연산은 정규 부동 소수점보다 수백에서 수백 배 느릴 수 있습니다 . 많은 프로세서가 직접 처리 할 수 ​​없으므로 마이크로 코드를 사용하여 트랩하고 해결해야하기 때문입니다.

10,000 반복 후 번호를 인쇄하는 경우, 당신은 그들이에 여부에 따라 다른 값으로 수렴 한 것을 볼 수 0또는 0.1사용됩니다.

x64에서 컴파일 된 테스트 코드는 다음과 같습니다.

int main() {

    double start = omp_get_wtime();

    const float x[16]={1.1,1.2,1.3,1.4,1.5,1.6,1.7,1.8,1.9,2.0,2.1,2.2,2.3,2.4,2.5,2.6};
    const float z[16]={1.123,1.234,1.345,156.467,1.578,1.689,1.790,1.812,1.923,2.034,2.145,2.256,2.367,2.478,2.589,2.690};
    float y[16];
    for(int i=0;i<16;i++)
    {
        y[i]=x[i];
    }
    for(int j=0;j<9000000;j++)
    {
        for(int i=0;i<16;i++)
        {
            y[i]*=x[i];
            y[i]/=z[i];
#ifdef FLOATING
            y[i]=y[i]+0.1f;
            y[i]=y[i]-0.1f;
#else
            y[i]=y[i]+0;
            y[i]=y[i]-0;
#endif

            if (j > 10000)
                cout << y[i] << "  ";
        }
        if (j > 10000)
            cout << endl;
    }

    double end = omp_get_wtime();
    cout << end - start << endl;

    system("pause");
    return 0;
}

산출:

#define FLOATING
1.78814e-007  1.3411e-007  1.04308e-007  0  7.45058e-008  6.70552e-008  6.70552e-008  5.58794e-007  3.05474e-007  2.16067e-007  1.71363e-007  1.49012e-007  1.2666e-007  1.11759e-007  1.04308e-007  1.04308e-007
1.78814e-007  1.3411e-007  1.04308e-007  0  7.45058e-008  6.70552e-008  6.70552e-008  5.58794e-007  3.05474e-007  2.16067e-007  1.71363e-007  1.49012e-007  1.2666e-007  1.11759e-007  1.04308e-007  1.04308e-007

//#define FLOATING
6.30584e-044  3.92364e-044  3.08286e-044  0  1.82169e-044  1.54143e-044  2.10195e-044  2.46842e-029  7.56701e-044  4.06377e-044  3.92364e-044  3.22299e-044  3.08286e-044  2.66247e-044  2.66247e-044  2.24208e-044
6.30584e-044  3.92364e-044  3.08286e-044  0  1.82169e-044  1.54143e-044  2.10195e-044  2.45208e-029  7.56701e-044  4.06377e-044  3.92364e-044  3.22299e-044  3.08286e-044  2.66247e-044  2.66247e-044  2.24208e-044

두 번째 실행에서 숫자가 어떻게 0에 매우 가까운 지 확인하십시오.

비정규 화 된 숫자는 일반적으로 드물기 때문에 대부분의 프로세서는 효율적으로 처리하려고하지 않습니다.


코드의 시작 부분에 이것을 추가하여 비정규를 0 으로 플러시 하면 비정규 화 된 숫자와 관련이 있음을 보여주기 위해

_MM_SET_FLUSH_ZERO_MODE(_MM_FLUSH_ZERO_ON);

그런 다음 버전 0이 더 이상 10 배 더 느려지지 않고 실제로 더 빨라집니다. (이를 위해서는 SSE가 활성화 된 상태에서 코드를 컴파일해야합니다.)

이것은이 이상한 낮은 정밀도 거의 0 값을 사용하는 대신 우리는 단지 0으로 반올림한다는 것을 의미합니다.

타이밍 : Core i7 920 @ 3.5 GHz :

//  Don't flush denormals to zero.
0.1f: 0.564067
0   : 26.7669

//  Flush denormals to zero.
0.1f: 0.587117
0   : 0.341406

결국, 이것은 정수이든 부동 소수점이든 관계가 없습니다. 0또는이 0.1f두 루프의 레지스터 외부에 저장 / 변환된다. 따라서 성능에는 영향을 미치지 않습니다.


100
"+ 0"이 기본적으로 컴파일러에 의해 완전히 최적화되지 않았다는 것이 여전히 이상합니다. 그가 "+ 0.0f"를 넣었다면 이런 일이 있었을까요?
s73v3r

51
@ s73v3r 아주 좋은 질문입니다. 이제 어셈블리를 살펴본 결과 + 0.0f최적화 되지 않았습니다 . 내가 추측 + 0.0f해야한다면 y[i], 신호 NaN또는 무언가 가 발생하면 부작용이있을 수 있습니다 ...하지만 잘못 될 수 있습니다.
Mysticial

14
복식은 많은 경우에 단지 다른 숫자의 크기로 여전히 같은 문제가 발생합니다. 0으로 플러시는 오디오 응용 프로그램 (및 여기저기서 1e-38을 잃을 여유가있는 다른 응용 프로그램)에 적합하지만 x87에는 적용되지 않는다고 생각합니다. FTZ가 없으면 오디오 응용 프로그램의 일반적인 수정은 매우 낮은 진폭 (가청 불가능) DC 또는 구형파 신호를 주입하여 숫자를 비정규 성에서 멀리 떨어 뜨리는 것입니다.
Russell Borogove

16
y [i]가 0.1을 더한 값보다 더 작 으면 숫자에서 가장 큰 숫자가 높아지기 때문에 정밀도가 손실되기 때문에 @Isaac.
Dan은 Fidelling에 의해 Firelight에 의해

167
@ s73v3r : 부동 소수점은 음수가 0이므로 + 0.f를 최적화 할 수 없으며 + .f를 -.0f에 더한 결과는 + 0.f입니다. 따라서 0.f를 추가하는 것은 ID 작업이 아니며 최적화 할 수 없습니다.
Eric Postpischil

415

gcc생성 된 어셈블리에 diff를 사용 하고 적용 하면 다음 과 같은 차이점 만 나타납니다.

73c68,69
<   movss   LCPI1_0(%rip), %xmm1
---
>   movabsq $0, %rcx
>   cvtsi2ssq   %rcx, %xmm1
81d76
<   subss   %xmm1, %xmm0

cvtsi2ssq느린 실제로 10 배되는 하나.

분명히 float버전은 메모리에서로드 된 XMM 레지스터를 사용하는 반면, int버전은 실제 int값 0을 명령 을 float사용하여 변환하는 cvtsi2ssq데 많은 시간이 걸립니다. 전달 -O3GCC로하는 것은 도움이되지 않습니다. (gcc 버전 4.2.1.)

(를 로 변경한다는 점을 제외하고는 double대신 사용 하는 float것은 중요하지 않습니다 .)cvtsi2ssqcvtsi2sdq

최신 정보

일부 추가 테스트에 따르면 반드시 cvtsi2ssq지시 사항 은 아닙니다 . 를 제거하면 ( 대신에 a int ai=0;float a=ai;를 사용하여 사용 ) 속도 차이가 남아 있습니다. @Mysticial이 맞습니다. 비정규 화 된 수레가 차이를 만듭니다. 와 사이의 값을 테스트하면 알 수 있습니다 . 루프가 갑자기 10 배나 걸리는 위 코드의 전환점은 대략 입니다.a000.1f0.00000000000000000000000000000001

업데이트 << 1

이 흥미로운 현상의 작은 시각화 :

  • 1 열 : 모든 반복에 대해 2로 나눈 float
  • 2 열 :이 플로트의 이진 표현
  • 3 열 :이 플로트를 합산하는 데 걸린 시간 1e7 회

비정규 화가 시작되면 지수 (마지막 9 비트)가 가장 낮은 값으로 변경되는 것을 볼 수 있습니다.이 시점에서 단순 덧셈은 20 배 느려집니다.

0.000000000000000000000000000000000100000004670110: 10111100001101110010000011100000 45 ms
0.000000000000000000000000000000000050000002335055: 10111100001101110010000101100000 43 ms
0.000000000000000000000000000000000025000001167528: 10111100001101110010000001100000 43 ms
0.000000000000000000000000000000000012500000583764: 10111100001101110010000110100000 42 ms
0.000000000000000000000000000000000006250000291882: 10111100001101110010000010100000 48 ms
0.000000000000000000000000000000000003125000145941: 10111100001101110010000100100000 43 ms
0.000000000000000000000000000000000001562500072970: 10111100001101110010000000100000 42 ms
0.000000000000000000000000000000000000781250036485: 10111100001101110010000111000000 42 ms
0.000000000000000000000000000000000000390625018243: 10111100001101110010000011000000 42 ms
0.000000000000000000000000000000000000195312509121: 10111100001101110010000101000000 43 ms
0.000000000000000000000000000000000000097656254561: 10111100001101110010000001000000 42 ms
0.000000000000000000000000000000000000048828127280: 10111100001101110010000110000000 44 ms
0.000000000000000000000000000000000000024414063640: 10111100001101110010000010000000 42 ms
0.000000000000000000000000000000000000012207031820: 10111100001101110010000100000000 42 ms
0.000000000000000000000000000000000000006103515209: 01111000011011100100001000000000 789 ms
0.000000000000000000000000000000000000003051757605: 11110000110111001000010000000000 788 ms
0.000000000000000000000000000000000000001525879503: 00010001101110010000100000000000 788 ms
0.000000000000000000000000000000000000000762939751: 00100011011100100001000000000000 795 ms
0.000000000000000000000000000000000000000381469876: 01000110111001000010000000000000 896 ms
0.000000000000000000000000000000000000000190734938: 10001101110010000100000000000000 813 ms
0.000000000000000000000000000000000000000095366768: 00011011100100001000000000000000 798 ms
0.000000000000000000000000000000000000000047683384: 00110111001000010000000000000000 791 ms
0.000000000000000000000000000000000000000023841692: 01101110010000100000000000000000 802 ms
0.000000000000000000000000000000000000000011920846: 11011100100001000000000000000000 809 ms
0.000000000000000000000000000000000000000005961124: 01111001000010000000000000000000 795 ms
0.000000000000000000000000000000000000000002980562: 11110010000100000000000000000000 835 ms
0.000000000000000000000000000000000000000001490982: 00010100001000000000000000000000 864 ms
0.000000000000000000000000000000000000000000745491: 00101000010000000000000000000000 915 ms
0.000000000000000000000000000000000000000000372745: 01010000100000000000000000000000 918 ms
0.000000000000000000000000000000000000000000186373: 10100001000000000000000000000000 881 ms
0.000000000000000000000000000000000000000000092486: 01000010000000000000000000000000 857 ms
0.000000000000000000000000000000000000000000046243: 10000100000000000000000000000000 861 ms
0.000000000000000000000000000000000000000000022421: 00001000000000000000000000000000 855 ms
0.000000000000000000000000000000000000000000011210: 00010000000000000000000000000000 887 ms
0.000000000000000000000000000000000000000000005605: 00100000000000000000000000000000 799 ms
0.000000000000000000000000000000000000000000002803: 01000000000000000000000000000000 828 ms
0.000000000000000000000000000000000000000000001401: 10000000000000000000000000000000 815 ms
0.000000000000000000000000000000000000000000000000: 00000000000000000000000000000000 42 ms
0.000000000000000000000000000000000000000000000000: 00000000000000000000000000000000 42 ms
0.000000000000000000000000000000000000000000000000: 00000000000000000000000000000000 44 ms

ARM에 대한 동등한 설명은 Objective-C의 스택 오버플로 질문 비정규 화 된 부동 소수점? .


27
-O수정하지는 않지만 해결합니다 -ffast-math. (나는 항상 그것을 사용한다. IMO가 정밀한 문제를 일으키는 코너 사례는 어쨌든 제대로 설계된 프로그램에서 나타나지 않아야한다.)
leftaroundabout February

gcc-4.6의 긍정적 인 최적화 수준에서는 전환이 없습니다.
Jed

@leftaroundabout : -ffast-mathMXCSR에서 FTZ (0으로 플러시) 및 DAZ (비정상은 0)를 설정하는 일부 추가 시작 코드 링크를 사용 하여 실행 파일 (라이브러리가 아님)을 컴파일 하므로 CPU는 비정상적으로 느린 마이크로 코드 지원을받을 필요가 없습니다.
Peter Cordes

34

비정규 화 된 부동 소수점 사용으로 인한 것입니다. 그것과 성능 저하를 제거하는 방법? 비정상적인 숫자를 죽이는 방법으로 인터넷을 sc이 뒤졌지만 아직이를 수행하는 가장 좋은 방법은없는 것 같습니다. 다른 환경에서 가장 잘 작동하는 다음 세 가지 방법을 찾았습니다.

  • 일부 GCC 환경에서는 작동하지 않을 수 있습니다.

    // Requires #include <fenv.h>
    fesetenv(FE_DFL_DISABLE_SSE_DENORMS_ENV);
  • 일부 Visual Studio 환경에서는 작동하지 않을 수 있습니다. 1

    // Requires #include <xmmintrin.h>
    _mm_setcsr( _mm_getcsr() | (1<<15) | (1<<6) );
    // Does both FTZ and DAZ bits. You can also use just hex value 0x8040 to do both.
    // You might also want to use the underflow mask (1<<11)
  • GCC와 Visual Studio 모두에서 작동하는 것으로 나타납니다.

    // Requires #include <xmmintrin.h>
    // Requires #include <pmmintrin.h>
    _MM_SET_FLUSH_ZERO_MODE(_MM_FLUSH_ZERO_ON);
    _MM_SET_DENORMALS_ZERO_MODE(_MM_DENORMALS_ZERO_ON);
  • 인텔 컴파일러에는 최신 인텔 CPU에서 기본적으로 비정상을 비활성화하는 옵션이 있습니다. 자세한 내용은 여기

  • 컴파일러 스위치. -ffast-math, -msse또는 -mfpmath=sse비정규을 해제하고 다른 몇 가지 더 빠르게,하지만 불행히도 또한 당신의 암호를 해독 할 수있는 다른 근사 많이 할 것입니다. 신중하게 테스트하십시오! Visual Studio 컴파일러의 빠른 계산과 동일 /fp:fast하지만 이것이 비정상을 비활성화하는지 여부를 확인할 수 없었습니다. 1


1
이것은 다르지만 관련 질문에 대한 적절한 대답처럼 들립니다 (숫자 계산으로 인해 비정상적인 결과가 생성되는 것을 어떻게 막을 수 있습니까?)하지만이 질문에 대답하지는 않습니다.
Ben Voigt 2016 년

Windows X64는 .exe를 시작할 때 갑작스러운 언더 플로 설정을 전달하지만 Windows 32 비트 및 Linux는 그렇지 않습니다. 리눅스에서 gcc -ffast-math는 갑작스러운 언더 플로를 설정해야합니다 (그러나 Windows에서는 그렇지 않다고 생각합니다). 인텔 컴파일러는 main ()에서 초기화되어 이러한 OS 차이가 발생하지 않지만 물렸으므로 프로그램에서 명시 적으로 설정해야합니다. Sandy Bridge로 시작하는 Intel CPU는 더하기 / 빼기 (분할 / 곱하기가 아님)에서 발생하는 하위 표준을 효율적으로 처리해야하므로 점진적인 언더 플로를 사용하는 경우가 있습니다.
tim18

1
Microsoft / fp : fast (기본값 아님)는 gcc -ffast-math 또는 ICL (기본값) / fp : fast 고유의 공격적인 작업을 수행하지 않습니다. ICL / fp : source와 비슷합니다. 따라서 이러한 컴파일러를 비교하려면 명시 적으로 / fp :( 및 경우에 따라 언더 플로 모드)를 설정해야합니다.
tim18

18

gcc에서는 다음을 사용하여 FTZ 및 DAZ를 활성화 할 수 있습니다.

#include <xmmintrin.h>

#define FTZ 1
#define DAZ 1   

void enableFtzDaz()
{
    int mxcsr = _mm_getcsr ();

    if (FTZ) {
            mxcsr |= (1<<15) | (1<<11);
    }

    if (DAZ) {
            mxcsr |= (1<<6);
    }

    _mm_setcsr (mxcsr);
}

또한 gcc 스위치를 사용하십시오. -msse -mfpmath = sse

(Carl Hetherington [1]의 해당 크레딧)

[1] http://carlh.net/plugins/denormals.php


또한 참조 fesetround()에서 fenv.h(C99에 대해 정의) (반올림의 이식성 방법, 서로에 대한 linux.die.net/man/3/fesetround을 (이 있지만) 뿐만 아니라 subnormals 모든 FP 작업에 영향을 줄 수 )
독일어 가르시아

FTZ에 1 << 15 및 1 << 11이 필요합니까? 나는 다른 곳에서 인용 된 1 << 15 만 보았습니다 ...
fig

@fig : 1 << 11은 언더 플로우 마스크입니다. 자세한 정보는 여기 : softpixel.com/~cwright/programming/simd/sse.php
German Garcia

@GermanGarcia 이것은 OP 질문에 대답하지 않습니다. 문제는 "이 비트 코드는 왜 실행 하는가보다 10 배 더 빠릅니다."입니다.

9

Dan Neely의 의견 은 다음과 같은 답변으로 확장되어야합니다.

0.0f비정규 화 된 0 상수가 아니 거나 속도 저하를 유발하며 루프의 각 반복마다 0에 접근하는 값입니다. 그것들이 0에 가까워 질수록 표현하기 위해 더 많은 정밀도가 필요하며 비정규 화됩니다. 이것이 y[i]값입니다. ( x[i]/z[i]모두 1.0보다 작기 때문에 0에 접근 합니다 i.)

코드의 느리고 빠른 버전의 중요한 차이점은 명령문 y[i] = y[i] + 0.1f;입니다. 이 행이 루프의 각 반복마다 실행되면 부동 소수점의 추가 정밀도가 손실되고 정밀도가 더 이상 필요하지 않음을 나타내는 비정규 화가 필요합니다. 그 후에 부동 소수점 연산 y[i]은 비정규 화되지 않기 때문에 빠르게 유지됩니다.

추가 할 때 왜 추가 정밀도가 손실 0.1f됩니까? 부동 소수점 숫자는 유효 숫자가 너무 많기 때문입니다. 최소 유효 비트를 저장할 공간이 없기 때문에 적어도 3 개의 유효 자릿수를 저장하기에 충분한 공간이 있다고 가정 0.00001 = 1e-5하고 0.00001 + 0.1 = 0.1,이 예에서는 float 형식의 경우 최소한 0.10001.

요컨대, y[i]=y[i]+0.1f; y[i]=y[i]-0.1f;당신이 생각하는 no-op가 아닙니다.

Mystical은 이것도 말했다 : 수레의 내용은 어셈블리 코드 뿐만 아니라 중요합니다.

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