2014-05-17 5 views
3

_mm_set_epi*을 피하고 _mm_load_si128 (또는 _mm_loadu_si128)을 사용하는 것이 가장 좋음을 이해했습니다. 데이터가 정렬되지 않은 경우 작은 실적이 발생합니다. 그러나 성능에 미치는 영향이 저와 일치하지 않는 것 같습니다. 다음은 좋은 예입니다.왜 _mm_set_epi16이 _mm_load_si128보다 빠른 이유는 무엇입니까?

static uint32_t clmul_load(uint16_t x, uint16_t y) 
{ 
    const __m128i c = _mm_clmulepi64_si128(
     _mm_load_si128((__m128i const*)(&x)), 
     _mm_load_si128((__m128i const*)(&y)), 0); 

    return _mm_extract_epi32(c, 0); 
} 

static uint32_t clmul_set(uint16_t x, uint16_t y) 
{ 
    const __m128i c = _mm_clmulepi64_si128(
     _mm_set_epi16(0, 0, 0, 0, 0, 0, 0, x), 
     _mm_set_epi16(0, 0, 0, 0, 0, 0, 0, y), 0); 

    return _mm_extract_epi32(c, 0); 
} 

다음 함수는 두 가지의 성능 벤치 마크 :

가 SSE의 내장 함수 사용 다음과 같은 두 가지 기능을 고려 마지막으로

template <typename F> 
void benchmark(int t, F f) 
{ 
    std::mt19937 rng(static_cast<unsigned int>(std::time(0))); 
    std::uniform_int_distribution<uint32_t> uint_dist10(
     0, std::numeric_limits<uint32_t>::max()); 

    std::vector<uint32_t> vec(t); 

    auto start = std::chrono::high_resolution_clock::now(); 

    for (int i = 0; i < t; ++i) 
    { 
     vec[i] = f(uint_dist10(rng), uint_dist10(rng)); 
    } 

    auto duration = std::chrono::duration_cast< 
     std::chrono::milliseconds>(
     std::chrono::high_resolution_clock::now() - 
     start); 

    std::cout << (duration.count()/1000.0) << " seconds.\n"; 
} 

을, 다음과 같은 주요 프로그램은 몇 가지 테스트를 수행합니다

int main() 
{ 
    const int N = 10000000; 
    benchmark(N, clmul_load); 
    benchmark(N, clmul_set); 
} 

MSVC 2013의 i7 Haswell에서 일반적인 출력 t은 일반적인 출력이 설명 무엇

0.312 seconds. // _mm_load_si128 
0.262 seconds. // _mm_set_epi16 

입니다 -O3 -std=c++11 -march=native (약간 오래된 하드웨어) 매개 변수와 함께 GCC를 사용

0.208 seconds. // _mm_load_si128 
0.129 seconds. // _mm_set_epi16 

입니까? 실제로 _mm_set_epi*_mm_load_si128 이상인 경우가 있습니까? 다른 경우에는 _mm_load_si128이 더 잘 수행되는 것으로 나타 났지만 실제로는 그 관찰을 특성화 할 수 없습니다.

+0

'_mm_insert_epi16' 시도해 볼 수 있습니다. '_mm_insert_epi16 (_mm_setzero_si128(), x, 0)'과 같은 것 - 정확히 맞는지 확실하지 않습니다. –

답변

5

컴파일러는 실제로 필요하지 않으므로 _mm_set_epi16() 호출의 "수집"동작을 최적화합니다. g ++ 4.8 (-03) 및 GDB에서 :

(gdb) disas clmul_load 
Dump of assembler code for function clmul_load(uint16_t, uint16_t): 
    0x0000000000400b80 <+0>:  mov %di,-0xc(%rsp) 
    0x0000000000400b85 <+5>:  mov %si,-0x10(%rsp) 
    0x0000000000400b8a <+10>: vmovdqu -0xc(%rsp),%xmm0 
    0x0000000000400b90 <+16>: vmovdqu -0x10(%rsp),%xmm1 
    0x0000000000400b96 <+22>: vpclmullqlqdq %xmm1,%xmm0,%xmm0 
    0x0000000000400b9c <+28>: vmovd %xmm0,%eax 
    0x0000000000400ba0 <+32>: retq 
End of assembler dump. 

(gdb) disas clmul_set 
Dump of assembler code for function clmul_set(uint16_t, uint16_t): 
    0x0000000000400bb0 <+0>:  vpxor %xmm0,%xmm0,%xmm0 
    0x0000000000400bb4 <+4>:  vpxor %xmm1,%xmm1,%xmm1 
    0x0000000000400bb8 <+8>:  vpinsrw $0x0,%edi,%xmm0,%xmm0 
    0x0000000000400bbd <+13>: vpinsrw $0x0,%esi,%xmm1,%xmm1 
    0x0000000000400bc2 <+18>: vpclmullqlqdq %xmm1,%xmm0,%xmm0 
    0x0000000000400bc8 <+24>: vmovd %xmm0,%eax 
    0x0000000000400bcc <+28>: retq 
End of assembler dump. 

vpinsrw (삽입 워드)으로 내부로드/저장 유닛 존재 항시 소위 약간 빠른 clmul_load에서 정렬되지 않은 더블 쿼드 워드 움직임보다 쉽다 동시에 작은 판독을 할 수는 있지만 16B 판독은 할 수 없습니다. 당신이 더 임의적 인 하중을가한다면, 이것은 분명히 사라질 것입니다.

+1

맞아요. 이런 종류의 경우에는'_mm_set_epi'를 사용하는 것이 합리적 일 수 있습니다. 그렇다면 컴파일러가 항상 이런 식으로 최적화하지 않을 것이라고 가정하는 것이 더 안전 할 수도 있습니다. 대신에'vpinsrw'를 사용하여 작성하십시오. – Gideon

+0

여기서는 컴파일러 최적화에 비교적 익숙 할 수 있다고 생각합니다. g ++에서'-O1'조차도 실제로 이것을 산출합니다. – Jeff

1

_mm_set_epi*의 속도 저하는 다양한 변수를 함께 단일 벡터로 긁어 모을 필요성에서 비롯됩니다. 생성 된 어셈블리를 검사해야 할 것입니다. 그러나 제 생각에 여러분의 _mm_set_epi16 호출에 대한 대부분의 인수는 상수 (및 0)이기 때문에 GCC는 해당 어셈블리에 대해 상당히 짧고 빠른 명령 집합을 생성합니다. 본질적인.