2012-07-04 2 views
10

SSE4 내적 제품으로이 코드를 개선하려고하지만 솔루션을 찾는 데 어려움을 겪고 있습니다. 이 함수는 각각 80 셀의 부동 배열을 포함하는 매개 변수 qi 및 tj를 얻은 다음 내적을 계산합니다. 반환 값은 네 점 곱을 갖는 벡터입니다. 그래서 제가하려고하는 것은 병렬로 20 개 값의 4 개 점 제품을 계산하는 것입니다.SSE4를 사용하여 점을 벡터화하는 작업

이 코드를 개선하는 방법에 대해 알고 계셨습니까?

inline __m128 ScalarProd20Vec(__m128* qi, __m128* tj) 
{ 
    __m128 res=_mm_add_ps(_mm_mul_ps(tj[0],qi[0]),_mm_mul_ps(tj[1],qi[1])); 
    res=_mm_add_ps(res,_mm_add_ps(_mm_mul_ps(tj[2],qi[2]),_mm_mul_ps(tj[3],qi[3]))); 
    res=_mm_add_ps(res,_mm_add_ps(_mm_mul_ps(tj[4],qi[4]),_mm_mul_ps(tj[5],qi[5]))); 
    res=_mm_add_ps(res,_mm_add_ps(_mm_mul_ps(tj[6],qi[6]),_mm_mul_ps(tj[7],qi[7]))); 
    res=_mm_add_ps(res,_mm_add_ps(_mm_mul_ps(tj[8],qi[8]),_mm_mul_ps(tj[9],qi[9]))); 
    res=_mm_add_ps(res,_mm_add_ps(_mm_mul_ps(tj[10],qi[10]),_mm_mul_ps(tj[11],qi[11]))); 
    res=_mm_add_ps(res,_mm_add_ps(_mm_mul_ps(tj[12],qi[12]),_mm_mul_ps(tj[13],qi[13]))); 
    res=_mm_add_ps(res,_mm_add_ps(_mm_mul_ps(tj[14],qi[14]),_mm_mul_ps(tj[15],qi[15]))); 
    res=_mm_add_ps(res,_mm_add_ps(_mm_mul_ps(tj[16],qi[16]),_mm_mul_ps(tj[17],qi[17]))); 
    res=_mm_add_ps(res,_mm_add_ps(_mm_mul_ps(tj[18],qi[18]),_mm_mul_ps(tj[19],qi[19]))); 
    return res; 
} 

답변

9

내가 지금까지 본 SSE 예제 중 수백 가지가 처음부터 이미 꽤 좋은 모양의 코드 중 하나입니다. SSE4 내적 명령은 필요하지 않습니다. (당신은 잘 할 수 있습니다!)

그러나, 넌 할 수있어 한 가지입니다 : (. 내가 아직 시간이 초과하지 않았기 때문에 시도라고)

는 현재 당신이 데이터 의존성 체인이 res. 벡터 추가는 오늘날 대부분의 기계에서 3-4 사이클입니다.

__m128 res0 = _mm_add_ps(_mm_mul_ps(tj[ 0],qi[ 0]),_mm_mul_ps(tj[ 1],qi[ 1])); 
__m128 res1 = _mm_add_ps(_mm_mul_ps(tj[ 2],qi[ 2]),_mm_mul_ps(tj[ 3],qi[ 3])); 

res0 = _mm_add_ps(res0,_mm_add_ps(_mm_mul_ps(tj[ 4],qi[ 4]),_mm_mul_ps(tj[ 5],qi[ 5]))); 
res1 = _mm_add_ps(res1,_mm_add_ps(_mm_mul_ps(tj[ 6],qi[ 6]),_mm_mul_ps(tj[ 7],qi[ 7]))); 

res0 = _mm_add_ps(res0,_mm_add_ps(_mm_mul_ps(tj[ 8],qi[ 8]),_mm_mul_ps(tj[ 9],qi[ 9]))); 
res1 = _mm_add_ps(res1,_mm_add_ps(_mm_mul_ps(tj[10],qi[10]),_mm_mul_ps(tj[11],qi[11]))); 

res0 = _mm_add_ps(res0,_mm_add_ps(_mm_mul_ps(tj[12],qi[12]),_mm_mul_ps(tj[13],qi[13]))); 
res1 = _mm_add_ps(res1,_mm_add_ps(_mm_mul_ps(tj[14],qi[14]),_mm_mul_ps(tj[15],qi[15]))); 

res0 = _mm_add_ps(res0,_mm_add_ps(_mm_mul_ps(tj[16],qi[16]),_mm_mul_ps(tj[17],qi[17]))); 
res1 = _mm_add_ps(res1,_mm_add_ps(_mm_mul_ps(tj[18],qi[18]),_mm_mul_ps(tj[19],qi[19]))); 

return _mm_add_ps(res0,res1); 

을이 거의 중요한 인하 : 당신이 할 수있는 것은 노드 분할을 위해 res 변수를 다음과

(10 additions on critical path) * (3 cycles addps latency) = 30 cycles 

: 그래서 당신의 코드는이 있기 때문에 실행 30 회 최소를 취할 것 절반에 경로입니다. 부동 소수점 비 연관성 (non-associativity) 때문에 컴파일러가 수행하는이 최적화는 불법입니다.


4-way 노드 분할 및 AMD FMA4 명령어를 사용하는 대체 버전이 있습니다. fused-multiply 추가 기능을 사용할 수 없으면 자유롭게 분할하십시오. 위의 첫 번째 버전보다 여전히 좋을 수도 있습니다.

__m128 res0 = _mm_mul_ps(tj[ 0],qi[ 0]); 
__m128 res1 = _mm_mul_ps(tj[ 1],qi[ 1]); 
__m128 res2 = _mm_mul_ps(tj[ 2],qi[ 2]); 
__m128 res3 = _mm_mul_ps(tj[ 3],qi[ 3]); 

res0 = _mm_macc_ps(tj[ 4],qi[ 4],res0); 
res1 = _mm_macc_ps(tj[ 5],qi[ 5],res1); 
res2 = _mm_macc_ps(tj[ 6],qi[ 6],res2); 
res3 = _mm_macc_ps(tj[ 7],qi[ 7],res3); 

res0 = _mm_macc_ps(tj[ 8],qi[ 8],res0); 
res1 = _mm_macc_ps(tj[ 9],qi[ 9],res1); 
res2 = _mm_macc_ps(tj[10],qi[10],res2); 
res3 = _mm_macc_ps(tj[11],qi[11],res3); 

res0 = _mm_macc_ps(tj[12],qi[12],res0); 
res1 = _mm_macc_ps(tj[13],qi[13],res1); 
res2 = _mm_macc_ps(tj[14],qi[14],res2); 
res3 = _mm_macc_ps(tj[15],qi[15],res3); 

res0 = _mm_macc_ps(tj[16],qi[16],res0); 
res1 = _mm_macc_ps(tj[17],qi[17],res1); 
res2 = _mm_macc_ps(tj[18],qi[18],res2); 
res3 = _mm_macc_ps(tj[19],qi[19],res3); 

res0 = _mm_add_ps(res0,res1); 
res2 = _mm_add_ps(res2,res3); 

return _mm_add_ps(res0,res2); 
+3

생각 해보니. 40 개의 메모리가로드됩니다. Sandy Bridge 프로세서를 사용하지 않는 한 40 사이클의 병목 현상이 발생합니다. 따라서 OP의 코드가 이미 최적 일 수 있습니다. – Mysticial

+2

부동 소수점 연관성에 관하여 : 컴파일러 플래그'-phast-math'의 종종 저평가되고 오해 된 검은 양은 가끔은 이상하게 작동합니다. 그리고 AMD는 인류의 거의 새벽부터 사이클 당 두 개의 L1 메모리로드를 할 수 있지만 불행히도 다른 곳에서는 개가 느립니다. – hirschhornsalz

+0

도움을 주셔서 대단히 감사드립니다. 내 테스트 결과에 의하면 내 코드는 아이디어만큼 빨리 작동한다고 나와 있습니다 (주석에서 언급 한대로). AMD FMA4는 재미있어 보이지만이 명령어는 내 컴퓨터에서 사용할 수 없으며 코드는 SSE2와 호환되어야합니다. 나는 그것을 절친한 수학으로 시도 할 것이다. –

3

먼저 가장 중요한 최적화는 컴파일러가 모든 최적화 설정을 사용하도록 설정하는 것입니다. 루프로 작성하는 경우, 그것을 풀다 가능성이 있으므로


컴파일러는 꽤 똑똑 : 당신이 -funroll-loops을 통과해야

__128 res = _mm_setzero(); 
for (int i = 0; i < 10; i++) { 
    res = _mm_add_ps(res, _mm_add_ps(_mm_mul_ps(tj[2*i], qi[2*i]), _mm_mul_ps(tj[2*i+1], qi[2*i+1]))); 
} 
return res; 

(GCC로하고 그것을 풀다거야 .) 한 번에 5 반복을 수행하는, 예를 들어

또한 매크로를 정의 할 수 루프 버전이 느린 경우, 손으로 풀다 :

__128 res = _mm_setzero(); 

#define STEP(i) res = _mm_add_ps(res, _mm_add_ps(_mm_mul_ps(tj[2*i], qi[2*i]), _mm_mul_ps(tj[2*i+1], qi[2*i+1]))) 

STEP(0); STEP(1); STEP(2); STEP(3); STEP(4); 
STEP(5); STEP(6); STEP(7); STEP(8); STEP(9); 

#undef STEP 

return res; 
01 23,516,

심지어 20-0에서 루프를 실행 (또는 매크로 버전과 동일한 기능을 수행 할) 수, 즉 :

__128 res = _mm_setzero(); 
for (int i = 0; i < 20; i++) { 
    res = _mm_add_ps(res, _mm_mul_ps(tj[i], qi[i])); 
} 
return res; 

(GCC와 -funroll-loops이 한 번에 10 번 반복 할 풀린다 즉 위의 2 대시 루프와 동일합니다.)

2

데이터가 특수 SSE4 내적 제품 지침 (dpps)에 적합한 형식으로 메모리에 정렬되어 있지 않습니다. 이러한 지침은 다음과 같이 ,, 하나의 벡터의 크기는 인접 기대 :

| v0-dim0 | v1-dim0 | v2-dim0 | v3-dim0 | v0-dim1 | ... 

현재 일반적인 접근 방식은 적절한 것 같다 : 데이터가 서로 인터리브 벡터가 나타나는 반면

| dim0 | dim1 | dim2 | ... | dim19 | 

곱셈의 결과가 생성 된 직후에 사용되지 않도록 지침을 재정렬함으로써 상황을 개선 할 수 있지만 실제로 컴파일러는 자체적으로이를 알아낼 수 있어야합니다.

관련 문제