C++ 为什么设置epi16有时比加载速度快?

C++ 为什么设置epi16有时比加载速度快?,c++,sse,intrinsics,C++,Sse,Intrinsics,我知道最好避免\u mm\u set\u epi*,而是依赖\u mm\u load\u si128(如果数据不对齐,甚至\u mm\u loadu\u si128会对性能造成小的影响)。然而,这对性能的影响在我看来并不一致。下面是一个很好的例子 考虑使用SSE内部函数的以下两个函数: static uint32_t clmul_load(uint16_t x, uint16_t y) { const __m128i c = _mm_clmulepi64_si128( _mm

我知道最好避免
\u mm\u set\u epi*
,而是依赖
\u mm\u load\u si128
(如果数据不对齐,甚至
\u mm\u loadu\u 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);
}
以下函数对这两个函数的性能进行了基准测试:

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";
}
在配备MSVC 2013的i7 Haswell上,典型输出为

0.208 seconds.  // _mm_load_si128
0.129 seconds.  // _mm_set_epi16
0.312 seconds.  // _mm_load_si128
0.262 seconds.  // _mm_set_epi16
使用带有参数的GCC
-O3-std=c++11-march=native
(使用稍旧的硬件),典型的输出是

0.208 seconds.  // _mm_load_si128
0.129 seconds.  // _mm_set_epi16
0.312 seconds.  // _mm_load_si128
0.262 seconds.  // _mm_set_epi16

这是怎么解释的?是否确实存在这样的情况,
\u mm\u set\u epi*
\u mm\u load\u si128
更可取?还有一些时候,我注意到
\u mm\u load\u si128
性能更好,但我无法真正描述这些观察结果。

缓慢的
\u mm\u set\u epi*
来自于需要将各种变量拼凑成一个向量。您必须检查生成的程序集才能确定,但我的猜测是,由于
\u mm\u set\u epi16
调用的大多数参数都是常量(以及零),GCC正在为内部生成一组相当短且快速的指令。

您的编译器正在优化“聚集”
\u mm\u set\u epi16()
调用的行为,因为它确实不需要。根据g++4.8(-O3)和gdb:

(gdb)disas clmul\U加载
函数clmul_加载(uint16_t,uint16_t)的汇编程序代码转储:
0x0000000000400b80:mov%di,-0xc(%rsp)
0x0000000000400b85:mov%si,-0x10(%rsp)
0x0000000000400b8a:vmovdqu-0xc(%rsp),%xmm0
0x0000000000400b90:vmovdqu-0x10(%rsp),%xmm1
0x0000000000400b96:vpclmullqlqdq%xmm1,%xmm0,%xmm0
0x0000000000400b9c:vmovd%xmm0,%eax
0x0000000000400ba0:retq
汇编程序转储结束。
(gdb)disas clmul_集
函数clmul_集(uint16_t,uint16_t)的汇编程序代码转储:
0x0000000000400bb0:vpxor%xmm0,%xmm0,%xmm0
0x0000000000400bb4:vpxor%xmm1,%xmm1,%xmm1
0x0000000000400bb8:vpinsrw$0x0,%edi,%xmm0,%xmm0
0x0000000000400bbd:vpinsrw$0x0,%esi,%xmm1,%xmm1
0x0000000000400bc2:vpclmullqlqdq%xmm1,%xmm0,%xmm0
0x0000000000400bc8:vmovd%xmm0,%eax
0x0000000000400bcc:retq
汇编程序转储结束。

vpinsrw
(插入字)比未对齐的双四字从clmul_加载移动的速度稍快,这可能是因为内部加载/存储单元能够同时执行较小的读取,但不能执行16B的读取。如果您要执行更多的任意加载,这显然会消失。

对,因此在这种情况下,使用
\u mm\u set\u epi
可能是有意义的。再说一次,假设编译器不会总是这样优化它可能更安全,而应该首先使用
vpinsrw
来编写它。我认为您对这里的编译器优化比较满意。实际上,即使是g++中的
-O1
也会产生这种效果。您可以尝试
\u mm\u insert\u epi16
。类似于
\u mm\u insert\u epi16(\u mm\u setzero\u si128(),x,0)
-不确定这是否完全正确。