2016-07-27 4 views
1

popcnt와 SSE4.2를 사용하는 cpu에서 배열의 근사 역수 제곱근을 더 빨리 계산하는 방법은 무엇입니까?배열의 더 빠른 근사 역수 제곱근

입력 값은 float 배열에 저장된 양의 정수 (0부터 약 200,000 범위)입니다.

출력은 실수 배열입니다.

두 어레이의 sse에 대한 올바른 메모리 정렬이 있습니다.

코드

아래 만 1 개 XMM 레지스터를 사용하는 리눅스에서 실행하고,

gcc -O3 code.cpp -lrt -msse4.2 당신을 감사로 컴파일 할 수 있습니다.

#include <iostream> 
#include <emmintrin.h> 
#include <time.h> 

using namespace std; 
void print_xmm(__m128 xmm){ 
    float out[4]; 
    _mm_storeu_ps(out,xmm); 
    int i; 
    for (i = 0; i < 4; ++i) std::cout << out[i] << " "; 
    std::cout << std::endl; 
} 

void print_arr(float* ptr, size_t size){ 
    size_t i; 
    for(i = 0; i < size; ++i){ 
     cout << ptr[i] << " "; 
    } 
    cout << endl; 
} 

int main(void){ 
    size_t size = 25000 * 4; 
     // this has to be multiple of 4 
    size_t repeat = 10000; 
     // test 10000 cycles of the code 
    float* ar_in = (float*)aligned_alloc(16, size*sizeof(float)); 
    float* ar_out = (float*)aligned_alloc(16, size*sizeof(float)); 
    //fill test data into the input array 
    //the data is an array of positive numbers. 
    size_t i; 
    for (i = 0; i < size; ++i){ 
     ar_in[i] = (i+1) * (i+1); 
    } 
    //prepare for recipical square root. 
    __m128 xmm0; 
    size_t size_fix = size*sizeof(float)/sizeof(__m128); 
    float* ar_in_end = ar_in + size_fix; 
    float* ar_out_now; 
    float* ar_in_now; 
    //timing 
    struct timespec tp_start, tp_end; 
    i = repeat; 
    clock_gettime(CLOCK_MONOTONIC, &tp_start); 
    //start timing 
    while(--i){ 
     ar_out_now = ar_out; 
     for(ar_in_now = ar_in; 
      ar_in_now != ar_in_end; 
      ar_in_now += 4, ar_out_now+=4){ 
      //4 = sizeof(__m128)/sizeof(float); 
      xmm0 = _mm_load_ps(ar_in_now); 
      //cout << "load xmm: "; 
      //print_xmm(xmm0); 
      xmm0 = _mm_rsqrt_ps(xmm0); 
      //cout << "rsqrt xmm: "; 
      //print_xmm(xmm0); 
      _mm_store_ps(ar_out_now,xmm0); 
     } 
    } 
    //end timing 
    clock_gettime(CLOCK_MONOTONIC, &tp_end); 
    double timing; 
    const double nano = 0.000000001; 

    timing = ((double)(tp_end.tv_sec - tp_start.tv_sec) 
      + (tp_end.tv_nsec - tp_start.tv_nsec) * nano)/repeat; 

    cout << " timing per cycle: " << timing << endl; 
    /* 
    cout << "input array: "; 
    print_arr(ar_in, size); 
    cout << "output array: "; 
    print_arr(ar_out,size); 
    */ 
    //free mem 
    free(ar_in); 
    free(ar_out); 
    return 0; 
} 
+3

skylake와 같은 것으로,'rsqrtps'는 4 사이클 레이턴시를가집니다 만, 파이프 라인되고있어 새로운 rsqrtps가 매회 발행 될 수 있습니다. 루프를 최대 네 번 풀어서 속도를 향상시킬 수는 있지만 결과가 처리되지 않고 바로 저장되기 때문에 레지스터 이름 바꾸기 및 순서가 벗어난 실행은 아마 당신이 당신보다 훨씬 나아지지 않는다는 것을 의미합니다. 그래. – EOF

+0

이 코드의 병렬 버전을 작성하면 허위 공유를 제외한 모든 것에 대해 생각해야합니까? – rxu

+0

데이터 경쟁을 피하십시오. 예를 들어 배열을 분할하는 경우 겹침이 없는지 확인하십시오. 동일한 결과를 동일한 장소에 두 번 쓰는 것이 좋다고 생각할 수도 있지만 배열을 원자 적으로 선언하지 않는 한 그렇지 않습니다. – EOF

답변

4

플로트 배열은 얼마나 큽니까? L1 (또는 L2), gcc5에서 이미 뜨겁다면.3 인텔 ® CPU에서 uop 처리량에 병목 현상을 일으키는이 코드는 3 개의 퓨즈가있는 도메인 uops를 사용하여 루프 당 하나의 벡터가 실행되므로 루프백이 발생합니다. (따라서 2 사이클 당 하나의 벡터에서 실행됩니다).

현대 인텔 CPU에서 1 벡터 처리량을 달성하려면 루프를 풀어야합니다. (언롤되지 않은 asm이 작동하지 않는 이유는 아래 참조). 컴파일러를 사용하면 C++ 소스에서 손으로 직접 작성하는 대신에 좋은 방법 일 것입니다. 예 : 프로필 기반 최적화 (gcc -fprofile-use)를 사용하거나 맹목적으로 -funroll-loops을 사용하십시오.


클록 당 16 바이트는 이론상 하나의 코어로 주 메모리 대역폭을 포화 시키면 충분합니다. 그러나 IIRC Z Boson은 여러 개의 코어를 사용하여 더 나은 대역폭을 관찰했습니다. 이는 여러 개의 코어가 더 많은 요청을 처리하고 하나의 코어를 사용하지 않아도 메모리를 유휴 상태로 두지 않기 때문일 수 있습니다. 입력이 코어의 L2에서 뜨겁다면, 아마도 그 코어를 사용하여 데이터를 처리하는 것이 가장 좋습니다.

Haswell 이상에서는 클록 당로드되고 저장되는 16 바이트가 L1 캐시 대역폭의 절반에 불과하기 때문에 코어 당 최대 대역폭까지이 AVX 버전이 필요합니다.

메모리가 병목 현상이있는 경우, 특히 큰 배열에 여러 스레드를 사용하는 경우 you might want to do a Newton-Raphson iteration to get a nearly-full accuracy 1/sqrt(x). (하나의 스레드가 하나의로드 + 스토어를 클럭 당 유지할 수 없기 때문에 그렇습니다.)

또는 나중에이 데이터를로드 할 때 rsqrt을 즉시 사용하십시오. 그것은 매우 싸구려, 높은 처리량과 함께,하지만 여전히 FP 추가와 유사한 대기 시간. 캐쉬에 맞지 않는 큰 배열이라면 데이터에 대한 별도의 패스를 적게하여 계산 강도를 높이는 것이 큰 문제입니다. (Cache Blocking aka Loop Tiling도 가능하면 알고리즘의 여러 단계를 데이터의 캐시 크기 단위로 실행하십시오.)

가능한 경우 최후의 수단으로 캐시 우회 NT 저장소 만 사용하십시오 캐시를 효과적으로 사용하는 방법을 찾지 못합니다. 사용하려고하는 일부 데이터를 변환 할 수 있다면 훨씬 좋습니다. 따라서 다음에 사용할 때 캐시에 저장됩니다. (.L31 to jne .L31 on the Godbolt compiler explorer에서)


주요 루프는 6 인텔 SNB-가족 CPU에 대한 마이크로 연산, indexed addressing modes don't micro-fuse 때문이다. (이것은 불행히도 Agner Fog's microarch pdf에 아직 문서화되어 있지 않습니다.)

Nehalem은 3 개의 ALU uops가있는 4 개의 fused-domain uops이므로 Nehalem은 클럭 당 1로 실행해야합니다. 별도의 대상을 쓰고 싶어하기 때문에

.L31: # the main loop: 6 uops on SnB-family, 4 uops on Nehalem 
    rsqrtps xmm0, XMMWORD PTR [rbx+rax]  # tmp127, MEM[base: ar_in_now_10, index: ivtmp.51_61, offset: 0B] 
    movaps XMMWORD PTR [rbp+0+rax], xmm0  # MEM[base: ar_out_now_12, index: ivtmp.51_61, offset: 0B], tmp127 
    add  rax, 16 # ivtmp.51, 
    cmp  rax, 100000  # ivtmp.51, 
    jne  .L31  #, 

, 그것은 줄이기없이 클럭 당 하나 개의 벡터에서 실행할 수 있도록 아래 4 융합 영역의 마이크로 연산에 루프를 얻을 수있는 방법은 없습니다. (로드 및 저장소는 모두 단일 레지스터 어드레싱 모드 여야하므로 이 대신 증가하여 current_dst으로 인덱싱 된 트릭이 작동하지 않음).

gcc가 포인터 증가분을 사용하도록 C++을 수정하면 src와 dst를 증가시켜야하므로 하나의 uop 만 저장하게됩니다. 즉float *endp = start + length;for (p = start ; p < endp ; p+=4) {}

.loop: 
    rsqrtps xmm0, [rsi] 
    add  rsi, 16 
    movaps [rdi], xmm0 
    add  rdi, 16 
    cmp  rdi, rbx 
    jne  .loop 

희망의 GCC와 같은 루프 언 롤링하면서, 는 달리 rsqrtps + movaps들이 여전히 모드를 해결하는 자신의 4 융합 영역의 마이크로 연산 색인을 사용하십시오 될 경우 같은 것을 할 것입니다 만들 것입니다, 언 롤링이 없으면 루프 당 하나의 벡터에서 루프가 실행됩니다.

+1

흥미로운 점 중 하나 : Agner Fog의 지시 사항에 따르면 Intel Atom의 처리량에 대해 스칼라 rsqrtss를 사용하는 것이 더 낫습니다. 이는 사실이라면 재미 있습니다. – EOF

1

당신은 물론, 그것을 측정해야하지만, 역 제곱근 (매우 정확하지)를 계산하기 알려진 코드가, (VS2015 및 GCC 5.4.0로 테스트 https://www.beyond3d.com/content/articles/8/

float InvSqrt (float x) { 
    float xhalf = 0.5f*x; 
    int i = *(int*)&x; 
    i = 0x5f3759df - (i>>1); 
    x = *(float*)&i; 
    x = x*(1.5f - xhalf*x*x); 
    return x; 
} 

을 확인 SSE2로) 변환 link

__m128 InvSqrtSSE2(__m128 x) { 
    __m128 xhalf = _mm_mul_ps(x, _mm_set1_ps(0.5f)); 

    x = _mm_castsi128_ps(_mm_sub_epi32(_mm_set1_epi32(0x5f3759df), _mm_srai_epi32(_mm_castps_si128(x), 1))); 

    return _mm_mul_ps(x, _mm_sub_ps(_mm_set1_ps(1.5f), _mm_mul_ps(xhalf, _mm_mul_ps(x, x)))); 
} 

UPDATE I

M Ea culpa! 부적절한 변환을 발견 한 @EOF에게 감사의 말을 전합니다.

+4

'rsqrtps'보다 더 빠를 수는 없습니다. 두 곱셈만으로도 더 느립니다. – harold

+0

@harold 나는 의심 스럽지만 측정에 관심이 있습니다. –

+1

당신이 인용 한 코드는 정의되지 않은 행동을합니다 C에서, 그리고 b) 정의되지 않은 행동의 요지는 'float'인수의 비트 표현을'int'로 해석하려고한다는 것입니다. – EOF

3

이것은 산술 강도가 매우 낮은 스트리밍 계산이므로 거의 확실하게 메모리 바운드입니다. 비 시간로드 및 저장을 사용하면 측정 속도가 빨라질 수 있습니다.

// Hint to the CPU that we don't want to use the cache 
// Be sure to update this if you use manual loop unrolling 
_mm_prefetch(reinterpret_cast<char*>(ar_in+4), _MM_HINT_NTA); 

// Hint to the CPU that we don't need to write back through the cache 
_mm_stream_ps(ar_out_now,xmm0); 


편집는 :

나는 다른 하드웨어에 어떻게 생겼는지 것을보고 몇 가지 테스트를 실행. 당연히 결과는 사용중인 하드웨어에 매우 민감합니다. 나는 현대 CPU에서 읽기/쓰기 버퍼 수가 증가한 것이 원인 일 가능성이 높습니다.

모든 코드를 사용하여 컴파일 된 GCC-6.1에서 2.5GHz @

gcc -std=c++14 -O3 -march=native -mavx -mfpmath=sse -ffast-math 

인텔 코어 i3-3120M과; 3MB 캐시

OP's code:    350 +- 10 milliseconds 
NTA Prefetch:   360 +- 5 milliseconds 
NTA Prefetch+NTA store: 430 +- 10 milliseconds 

Intel Core i7-6500U CPU @ 2.50GHz; 4메가바이트 캐시

OP's code:    205 +- 5 milliseconds 
NTA Prefetch:   220 +- 2 milliseconds 
NTA Prefetch+NTA store: 200 +- 5 milliseconds 

2메가바이트에 문제의 크기를 늘리면는 NTA 프리 페치 + NTA 저장소는 영업 이익의 솔루션을 통해 런타임에 ~ 30 % 감소를 본다.

결과 : 문제의 크기가 너무 작아 NTA로부터 실질적으로 이익을 얻을 수 없습니다. 구형 아키텍처에서는 유해합니다. 새로운 아키텍처에서는 OP의 솔루션과 동등합니다.

결론 :이 경우에는 별 도움이되지 않을 것입니다.

+1

음 ... 선형 RAM 액세스 패턴에 대한 수동 프리 페치는 이해가 가지 않겠지 만 다시 측정을보고 싶습니다 –

+0

프리 페치의 동작과 힌트에 대해 자세히 설명하지 않았습니다. 캐시에 데이터를 유지하기 위해 CPU에 연결합니다. 힌트가 사용되면 CPU는 제거하지 않습니다. – Tim

+2

'prefetchNTA'는 실제 하드웨어의 WB 메모리에 실제로 차이가 있습니까? 필자는 테스트하지 않았지만, 세트 연관 캐시의 MRU 위치 대신 LRU 위치에 새로로드 된 선을 삽입 할 수 있다고 생각합니다. [NT 답장 섹션은 여기를 참조하십시오] (http://stackoverflow.com/questions/35516878/acquire-release-semantics-with-non-temporal-stores-on-x64). 또한 OP의 문제 크기 인 6250 * 16 float * 4B/float는 L2 캐시 크기의 2/3에 불과합니다. 캐시를 우회하는 것은 아마도 나쁜 생각 일 것입니다. –