2017-02-02 22 views
0

벡터의 모든 요소를 ​​추가하기 위해 인라인 함수를 구현했지만 비 SIMD 추가보다 빠르지는 않습니다.내 AVX2 가로 덧셈 기능이 비 SIMD 추가보다 빠르지 않은 이유는 무엇입니까?

선언 :이 벡터의 모든 int 값을 추가 내 두 가지 방법

#define N 128 
#define M N 
int __attribute__((aligned(32)))temp8[8]; 
__m256i vec; 
int __attribute__((aligned(32))) c_result[N][M]; 

:

첫째, 비 SIMD 버전은 다음과 같습니다

_mm256_store_si256((__m256i *)&temp8[0] , vec); 
    c_result[i][j]= temp8[0]+temp8[1]+temp8[2]+temp8[3]+temp8[4]+temp8[5]+temp8[6]+temp8[7]; 

둘째, AVX2 버전 :

c_result[i][j] =_mm256_hadd2_epi32(vec); 

나는 이런 식으로 hadd2을 구현 :

// my horizontal addition of epi32 
    inline int _mm256_hadd2_epi32(__m256i a) 
    { 
    __m256i a_hi; 
    a_hi = _mm256_permute2x128_si256(a, a, 1); //maybe 1 should be 4 
    a = _mm256_hadd_epi32(a, a_hi); 
    a = _mm256_hadd_epi32(a, a); 
    a = _mm256_hadd_epi32(a, a); 
    return _mm256_extract_epi32(a,0); 
    } 

내가 gcc, Linux-mint, skylake 마이크로 아키텍처를 사용합니다.

는 나는 다음과 같은 이유로이 될 수 추측 : 이 특히 요소의 순서를 변경하는 적어도 하나의 사이클을 필요로 치환이 제한 벡터 실행 유닛과 달리 fastly 추가됩니다 스카이 레이크 마이크로 아키텍처의 정수 4 ALU 이어서 hadd 지침이 이어집니다. 문제는, 내가 놓친 것이거나 모든 요소를 ​​추가하기 위해 SIMD를 사용할 필요가 없다는 것입니다.

업데이트 : 방금 ​​MUL 프로그램을 저장소 에 추가하여 매트릭스 곱셈의 전체 코드에 액세스 할 수 있습니다. 비 -IMD 버전을 사용하면 경과 시간은 201 ns가되고 SIMD 버전에서는210 ns가됩니다.

+1

한 걸음 물러서서 왜 이런 수평 작업을 할 필요가 있는지 물어보십시오. 그렇다면 정말 성능에 중대한 병목 현상이 생길 수 있습니다. 일반적으로 루프 후 수평 작업 만 수행하면됩니다 (예 : 일반적으로 중요하지 않은 축소의 최종 단계로서 성능 측면에서 중요합니다. –

+0

@PaulR, 당신 말이 맞아요. 이런 식으로 병목 현상이 발생하는 것은 아닙니다. 그리고 저는 비 SIMD 및 AVX2 버전의 성능을 직접 테스트하고 싶었습니다. 물론 그것은 연구 및 교육 목적을위한 것입니다. 하지만 그 대답이 나를 도울 것입니다. 왜냐하면 3,5,7 및 9 인접 요소가 병목 구역에 수평으로 추가되어야하는 Convolution Matrix Kernel을 구현했기 때문입니다. – Martin

+1

최적화는 그보다 조금 더 복잡합니다. Agner Fog 매뉴얼에 대한 링크는 x86 태그 위키를 참조하십시오. 프론트 엔드, 생성 된 uops, 포트, 지연 시간 및 침투를 고려해야합니다. * perf *로 코드를 프로파일 링 했습니까? –

답변

2

직관이 단계 ...

temp8[0]+temp8[1]+temp8[2]+temp8[3]+temp8[4]+temp8[5]+temp8[6]+temp8[7] 

는 벡터화가 가속화해야한다는 비싼 부분이지만, 아마 잘못된 것 일 수 있습니다. 추가는 단일 muop이며, 레지스터가 아닌 (메모리가 아닌) 최근 x64 컴퓨터에서주기 당 4 회 실행할 수 있습니다. 따라서, 이론적으로, 프로세서는

주기 1.

temp8[0]+temp8[1] 
temp8[2]+temp8[3] 
temp8[4]+temp8[5] 
temp8[6]+temp8[7] 

주기 2

(temp8[0]+temp8[1])+(temp8[2]+temp8[3]) 
(temp8[4]+temp8[5])+(temp8[6]+temp8[7]) 

...이 작업을 수행 할 수 있으며, 여유 용량, 사이클 3에 대한 답변을 얻을. (우리의 프로세서는 수퍼 스칼라이며 순서가 잘못된 파이프 라인이 있습니다. 따라서 마법이 발생합니다.)

벡터화 된 접근법은 얼마나 빨라질 수 있습니까? 당신은 우리에게 해답을 준 ...

a = _mm256_hadd_epi32(a, a_hi); 
a = _mm256_hadd_epi32(a, a); 
a = _mm256_hadd_epi32(a, a); 

우리는 3주기 ... 물론, 그것은 아마도 저렴 보이는 ...하지만 _mm256_hadd_epi32 고유은 PHADD 명령입니다 (아래 아마도 무엇인가를 인식 할 수 있습니다 ~ 3 muops, 2 사이클마다 1 명령). 중요한 점은 프로세서가 여러 개의 스칼라 추가를 동시에 수행 할 수있는 동안 동시에 내장 함수 중 몇 가지를 동시에 실행할 수 없다는 점입니다. 따라서 더 빠르다는 것은 기술적 문제가된다는 것을 알 수 있습니다.

아무튼 내 대답을 요약하면 ...이 예제에서는 벡터화가 도움이 될 것이라고 기대하지 말아야합니다 (적어도 도움이되지는 않음). 싼 지침 (추가)의 슈퍼 스칼라 실행에 맞서기 때문입니다.

부록. 이 코드

_mm256_store_si256((__m256i *)&temp8[0] , vec); 
    c_result[i][j]= temp8[0]+temp8[1]+temp8[2]+temp8[3]+temp8[4]+temp8[5]+temp8[6]+temp8[7]; 

아마도 당신이 생각하는대로 컴파일되지 않습니다. 우리가 함수로

uint32_t hadd32(__m256i vector) { 
    uint32_t buffer[sizeof(__m256i)/sizeof(uint32_t)]; 
_mm256_store_si256((__m256i *)buffer , vector); 
uint32_t answer = buffer[0]+buffer[1]+buffer[2]+buffer[3]+buffer[4]+buffer[5]+buffer[6]+buffer[7]; 
return answer; 
} 

여러 컴파일러 (그 소리, GCC 7)을 물로 씻어 보자,

우리는 추가를 인식
vpextrd edx, xmm0, 1 
    vmovd eax, xmm0 
    add  eax, edx 
    vpextrd edx, xmm0, 2 
    add  eax, edx 
    vpextrd edx, xmm0, 3 
    vextracti128 xmm0, ymm0, 0x1 
    add  eax, edx 
    vmovd edx, xmm0 
    add  eax, edx 
    vpextrd edx, xmm0, 1 
    add  eax, edx 
    vpextrd edx, xmm0, 2 
    add  eax, edx 
    vpextrd edx, xmm0, 3 
    add  eax, edx 

이를 컴파일하지만, 어디 완전히 무시되었습니다로 임시 버퍼 vpextrd 호를 선호합니다. 여기서 교훈은 항상 생성 된 어셈블리를 살펴 보는 것입니다.