c++ - 0.1f에서 0으로 변경하면 성능이 10 배 느려지는 이유는 무엇입니까?




performance visual-studio-2010 (4)

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] 1

왜이 코드는,

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로 컴파일 할 때. (나는 다른 컴파일러로 테스트하지 않았다.)


이는 비정규 화 된 부동 소수점 사용 때문입니다. 그것과 성과 처벌을 모두 제거하는 방법? 비정상적인 숫자를 죽이는 방법을 찾기 위해 인터넷을 샅샅이 뒤졌지만, 아직 그렇게하는 데 "최선의"방법이없는 것 같습니다. 다른 환경에서 가장 잘 작동 할 수있는이 세 가지 방법을 발견했습니다.

  • 일부 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에서 기본적으로 비정품을 비활성화하는 옵션이 있습니다. 자세한 내용은 여기에

  • 컴파일러 스위치. -msse -ffast-math , -msse 또는 -mfpmath=sse-mfpmath=sse 을 비활성화하고 다른 몇 가지 작업을 빠르게 수행하지만 불행히도 코드를 손상시킬 수있는 많은 다른 근사값을 수행합니다. 신중하게 시험하십시오! Visual Studio 컴파일러의 fast-math와 동일한 기능은 /fp:fast 이지만이 기능이 비정품을 비활성화하는지 여부를 확인할 수 없었습니다. 1


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

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

cvtsi2ssq 는 실제로 10 배 더 느립니다.

분명히, float 버전은 메모리에서로드 된 XMM 레지스터를 사용하지만 int 버전은 많은 시간이 소요되는 cvtsi2ssq 명령어를 사용하여 실수 int 값 0을 float 로 변환합니다. gcc에 -O3 을 전달해도 도움이되지 않습니다. (gcc 버전 4.2.1)

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

최신 정보

일부 추가 테스트는 반드시 cvtsi2ssq 명령이 아님을 보여줍니다. 일단 제거되면 ( int ai=0;float a=ai; 를 사용 a 0 대신에 a 를 사용) 속도 차이가 유지됩니다. 따라서 @Mysticial은 맞습니다. 비정규 화 된 수레가 차이를 만듭니다. 이것은 00.1f 사이의 값을 테스트하여 볼 수 있습니다. 위 코드의 전환점은 루프가 갑자기 10 배 길어질 때 약 0.00000000000000000000000000000001 입니다.

업데이트 << 1

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

  • 열 1 : 모든 반복에 대해 2로 나눈 부동 소수점
  • 2 열 :이 float의 2 진 표현
  • 열 3 :이 플롯을 합계하는 데 걸리는 시간 1e7 times

비정규 화가 설정되면 지수 (마지막 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의 스택 오버플로 질문 비정규 부동 소수점에서 찾을 수 있습니까? .


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

비정규 (또는 비정규) 숫자는 부동 소수점 표현에서 제로에 가깝게 일부 추가 값을 얻기위한 종류의 해킹입니다. 비정규 화 된 부동 소수점 연산은 정규화 된 부동 소수점 연산보다 수십 배에서 수백 배 느려질 수 있습니다. 이는 많은 프로세서가 직접 처리 할 수 ​​없으며 마이크로 코드를 사용하여 처리하고 해결해야하기 때문입니다.

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으로 반올림합니다.

타이밍 : 코어 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 는 두 루프 외부의 레지스터로 변환 / 저장됩니다. 따라서 성능에 영향을 미치지 않습니다.







floating-point