为什么_mm_set_epi16有时比_mm_load_si128更快?

3
我知道最好避免使用_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);
}

下面的函数对这两种方法进行性能基准测试:
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上,典型的输出结果是:
0.208 seconds.  // _mm_load_si128
0.129 seconds.  // _mm_set_epi16

使用参数-O3 -std=c++11 -march=native(对于略旧的硬件)与GCC一起使用,典型的输出结果如下:

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

为什么会出现这种情况?是否确实存在一些情况下,_mm_set_epi*_mm_load_si128更优?我还注意到有些时候_mm_load_si128的表现更好,但我无法具体描述这些观察结果。


你可以尝试使用_mm_insert_epi16。类似这样的 _mm_insert_epi16(_mm_setzero_si128(),x,0) - 不确定是否完全正确。 - Z boson
2个回答

5

你的编译器正在优化掉你的_mm_set_epi16()调用中的 "gather" 行为,因为它并不是必需的。

从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.

vpinsrw(插入字)比来自clmul_load的非对齐双重四字移动略快,这可能是由于内部加载/存储单元能够同时进行较小读取但不能进行16B读取。如果您要执行更多的任意加载操作,显然这个优势就会消失。


1
好的,所以在这种情况下,使用 _mm_set_epi 可能是有意义的。但是,假设编译器不总是像这样进行优化,而是一开始就使用 vpinsrw 来编写可能更加安全。 - Gideon
我认为你可以对编译器优化相对放心。实际上,即使在g++中使用-O1也能产生这种效果。 - Jeff

1
< p > _mm_set_epi* 的缓慢性来自于需要将各种变量组合成单个向量。你需要检查生成的汇编代码才能确定,但我猜测由于大多数参数传递给你的 _mm_set_epi16 调用都是常量(而且是零),因此GCC会为内置函数生成一个相当简短和快速的指令集。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接