为什么_mm_set_epi16有时比_mm_load_si128快?

Gid*_*eon 3 c++ sse intrinsics

我知道最好避免_mm_set_epi*,而是依赖_mm_load_si128(_mm_loadu_si128如果数据没有对齐,甚至会有很小的性能损失).但是,这对性能的影响似乎与我不一致.以下是一个很好的例子.

考虑使用SSE内在函数的以下两个函数:

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);
}
Run Code Online (Sandbox Code Playgroud)

以下功能对两者的性能进行了基准测试:

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";
}
Run Code Online (Sandbox Code Playgroud)

最后,以下主程序进行了一些测试:

int main()
{
    const int N = 10000000; 
    benchmark(N, clmul_load);
    benchmark(N, clmul_set);
}
Run Code Online (Sandbox Code Playgroud)

在具有MSVC 2013的i7 Haswell上,典型的输出是

0.208 seconds.  // _mm_load_si128
0.129 seconds.  // _mm_set_epi16
Run Code Online (Sandbox Code Playgroud)

使用GCC参数-O3 -std=c++11 -march=native(稍微较旧的硬件),典型的输出是

0.312 seconds.  // _mm_load_si128
0.262 seconds.  // _mm_set_epi16
Run Code Online (Sandbox Code Playgroud)

这解释了什么?实际上是否有_mm_set_epi*更好的情况_mm_load_si128?还有一些时候我注意到_mm_load_si128表现更好,但我无法真正描述这些观察结果.

Jef*_*eff 5

您的编译器正在优化您的_mm_set_epi16()呼叫的"聚集"行为,因为它确实不需要.从g ++ 4.8(-O3)和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.
Run Code Online (Sandbox Code Playgroud)

vpinsrw(插入字)比从clmul_load,可能是未对齐的双四字举动不断所谓稍微快一些,由于内部加载/存储单元能够做到同时读取,但不是那些16B较小.如果你做了更多的任意负载,显然会消失.