2010-12-09 4 views
4

elmaelmc은 모두 unsigned long 어레이입니다. 따라서 res1res2입니다.SIMD 코드가 스칼라 코드보다 느리게 실행됩니다.

unsigned long simdstore[2]; 
__m128i *p, simda, simdb, simdc; 
p = (__m128i *) simdstore; 

for (i = 0; i < _polylen; i++) 
{ 
    u1 = (elma[i] >> l) & 15; 
    u2 = (elmc[i] >> l) & 15; 
    for (k = 0; k < 20; k++) 
    { 
     //res1[i + k] ^= _mulpre1[u1][k]; 
     //res2[i + k] ^= _mulpre2[u2][k];    

     simda = _mm_set_epi64x (_mulpre2[u2][k], _mulpre1[u1][k]); 
     simdb = _mm_set_epi64x (res2[i + k], res1[i + k]); 
     simdc = _mm_xor_si128 (simda, simdb); 
     _mm_store_si128 (p, simdc); 
     res1[i + k] = simdstore[0]; 
     res2[i + k] = simdstore[1];      
    }  
} 

for 루프 내에는 요소의 XOR의 비 simd 및 simd 버전이 모두 포함됩니다. 두 번째 for 루프 내의 처음 두 행은 명시 적 XOR을 수행하지만 나머지는 동일한 연산의 simd 버전을 구현합니다.

이 루프는 외부에서 수백 번 호출되므로이 루프를 최적화하면 총 계산 시간이 단축됩니다.

문제는 simd 코드가 스칼라 코드보다 느린 속도로 실행됩니다.

편집 : 부분 완료

__m128i *p1, *p2, *p3, *p4; 
p1 = (__m128i *) simdstore1; 
p2 = (__m128i *) simdstore2; 
p3 = (__m128i *) simdstore3; 
p4 = (__m128i *) simdstore4; 

for (i = 0; i < 20; i++) 
{ 
    u1 = (elma[i] >> l) & 15; 
    u2 = (elmc[i] >> l) & 15; 
    for (k = 0; k < 20; k = k + 4) 
    { 
     simda1 = _mm_set_epi64x (_mulpre2[u2][k], _mulpre1[u1][k]); 
     simda2 = _mm_set_epi64x (_mulpre2[u2][k + 1], _mulpre1[u1][k + 1]); 
     simda3 = _mm_set_epi64x (_mulpre2[u2][k + 2], _mulpre1[u1][k + 2]); 
     simda4 = _mm_set_epi64x (_mulpre2[u2][k + 3], _mulpre1[u1][k + 3]); 

     simdb1 = _mm_set_epi64x (res2[i + k], res1[i + k]); 
     simdb2 = _mm_set_epi64x (res2[i + k + 1], res1[i + k + 1]); 
     simdb3 = _mm_set_epi64x (res2[i + k + 2], res1[i + k + 2]); 
     simdb4 = _mm_set_epi64x (res2[i + k + 3], res1[i + k + 3]); 

     simdc1 = _mm_xor_si128 (simda1, simdb1); 
     simdc2 = _mm_xor_si128 (simda2, simdb2); 
     simdc3 = _mm_xor_si128 (simda3, simdb3); 
     simdc4 = _mm_xor_si128 (simda4, simdb4); 

     _mm_store_si128 (p1, simdc1); 
     _mm_store_si128 (p2, simdc2); 
     _mm_store_si128 (p3, simdc3); 
     _mm_store_si128 (p4, simdc4); 

     res1[i + k]= simdstore1[0]; 
     res2[i + k]= simdstore1[1]; 
     res1[i + k + 1]= simdstore2[0]; 
     res2[i + k + 1]= simdstore2[1]; 
     res1[i + k + 2]= simdstore3[0]; 
     res2[i + k + 2]= simdstore3[1]; 
     res1[i + k + 3]= simdstore4[0]; 
     res2[i + k + 3]= simdstore4[1]; 
    } 
} 

그러나이 결과는 많이 변경되지 않습니다 줄이기; 여전히 스칼라 코드의 두 배가 걸립니다.

+0

정렬 RES1/RES2, 레지스터를 스위 즐링 (swizzle), 똑바로 그들에게 물품. simdstore를 제거하십시오. – EboMike

+0

res1/res2는 코드에서 사용한 부호없는 long 배열입니다. 16 바이트 단위로 정렬하여 무엇을 의미하는지 확실하지 않으며 직접 작성하십시오. – anup

+0

시작 주소가 16 바이트로 정렬되어 있는지 확인한 다음 _mm_store_si128을 사용하여 직접 작성하십시오. (분명히, 두 가지 결과를 가져 와서 벡터 레지스터를 섞어서 하나의 결과로 결합해야합니다.) 메모리에 기록한 다음 다시 읽으면 실속이 발생합니다. – EboMike

답변

6

면책 조항 : 저는 PowerPC 배경에서 왔으므로 여기에서 말하고있는 것은 완전한 호그 워쉬 일 수 있습니다. 그러나 결과에 즉시 액세스하려고하기 때문에 벡터 파이프 라인을 지연시키고 있습니다.

모든 것을 벡터 파이프 라인에 보관하는 것이 가장 좋습니다. vector에서 int 또는 float로 변환하거나 메모리에 결과를 저장하면 즉시 stalling됩니다.

SSE 또는 VMX를 처리 할 때 가장 적합한 작동 모드는로드, 프로세스, 저장입니다. 벡터 레지스터에 데이터를로드하고 모든 벡터 처리를 수행 한 다음 메모리에 저장하십시오.

다음과 같이 권장합니다. 여러 개의 __m128i 레지스터를 예약하고 루프를 여러 번 언로드 한 다음 저장하십시오.

편집 : 또한 언 롤링하고 res1과 res2를 16 바이트로 정렬하면이 simdstore 간접 참조 (아마도 LHS 및 다른 정지)를 거치지 않고 결과를 메모리에 직접 저장할 수 있습니다.

편집 : 명백한 것을 잊어 버렸습니다. 폴리 라인이 일반적으로 큰 경우 모든 반복에서 데이터 캐시 프리 페치를 수행하는 것을 잊지 마십시오.

+4

루프 언 롤링은 PowerPC에서 좋지만 현대 x86 CPU에서는 좋지 않습니다. 재생할 레지스터가 적고 CPU 자체가 작은 루프에 대해 언 롤링을 수행합니다. –

+2

잘 고맙습니다. 감사합니다. 이 특별한 경우에는 벡터 레지스터에서 출력을 구성하기 위해 적어도 한 번 언 롤링하는 것이 합리적이라고 생각합니다 (두 개의 연속 된 결과를 취해야하고이를 하나로 결합해야하기 때문에) - 가장 큰 문제 중 하나는 여기에 있습니다. simdstore -> res1/res2 간접 지정입니다. – EboMike

+0

@ Paul R 내 경험에 의하면 x86에서 루프를 풀면 실제로 유용합니다. 그것은 32 비트에서 프로파일 링 한 이후로 잠시 동안 있었지만, 확실히 x86-64에서 유용합니다 (64 비트에 더 많은 레지스터가 있음). 나는 지속적으로 스피드 업을하고있다. 케이스에 따라 옵티마이 저가 실제로 더 좋은 방법으로 순서를 바꾸는 변경을 제공합니다. 카운터 변수 또는 언 롤링으로 계산할 수있는 모든 루프에서 수행되는 다른 연산을 줄이는 것은 말할 것도 없습니다. – Apriori

2

저는 SIMD 전문가도 아니지만 데이터를 프리 페치하고 EboMike가 언급 한 데로 인해 이익을 얻을 수있는 것처럼 보입니다. res1과 res2를 하나의 정렬 된 배열 (구조체의 다른 용도에 따라 다름)에 병합 한 다음 추가 복사가 필요하지 않은 경우 res1 및 res2를 직접 병합하면 도움이 될 수도 있습니다.

+1

프리 페칭은 올바르게 진행하기가 어렵고 거의 도움이되지 않습니다. –

4

코드가 res1과 res2로 보이는 방식은 완전히 독립적 인 것으로 보입니다. 그러나 이들을 xor에 등록 할 때 같은 레지스터에 넣습니다.

다른 레지스터를 다음과 같이 사용할 것입니다 ( 벡터가 모두 정렬되어야 함).

__m128i x0, x1, x2, x3; 
for (i = 0; i < _polylen; i++) 
{ 

    u1 = (elma[i] >> l) & 15; 
    u2 = (elmc[i] >> l) & 15; 
    for (k = 0; k < 20; k+=2) 
    {  
     //res1[i + k] ^= _mulpre1[u1][k]; 
     x0= _mm_load_si128(&_mulpre1[u1][k]); 
     x1= _mm_load_si128(&res1[i + k]); 
     x0= _mm_xor_si128 (x0, x1); 
     _mm_store_si128 (&res1[i + k], x0); 
     //res2[i + k] ^= _mulpre2[u2][k];    
     x2= _mm_load_si128(&_mulpre2[u2][k]); 
     x3= _mm_load_si128(&res2[i + k]); 
     x2= _mm_xor_si128 (x2, x3); 
     _mm_store_si128 (&res2[i + k], x2); 
    }  
} 

참고 4 개의 레지스터 만 사용하고 있습니다. x86_64에서 x86 이상의 8 개 레지스터를 모두 사용하도록 수동으로 언로드 할 수 있습니다.

+0

+1, 나는이 글을 쓰는 중간에 있었다. –

+0

res1과 res2가 올바르게 정렬되었는지 확인한다. Btw, res1 첫 번째 8 바이트 레지스터 및 res2 다른 8 바이트 가져옵니다 생각 했나요? 두 번의 반복 작업을 수행하고 결과를 정리해야합니다. – EboMike

+0

@EboMike가 맞습니다. res1은 처음 8 바이트를 얻지 만 res2는 나머지 8 바이트를 얻습니다. @renick, 여러분이 구현 한 방식은 두 개의 별도 xors를 사용했기 때문에 SIMD의 추가 이점이 없으며 스칼라 코드와 동일합니다. – anup

4

수행중인로드 및 저장 장치의 수에 비례하여 계산이 거의 필요하지 않으므로 결과가 거의 없습니다. SIMD.이 경우 특히 64 비트 모드에서 사용할 수있는 x86-64 CPU가있는 경우 스칼라 코드를 사용하는 것이 더 유용 할 것입니다. 이렇게하면로드 및 저장 횟수가 줄어들어 현재 성능에서 가장 중요한 요인이됩니다.

(참고 :. 당신은 코어 2 이상을 사용하는 경우 특히 당신은 아마, 루프를 풀다하지한다)

+1

+1로드/매장을 지적합니다. – Apriori

관련 문제