问题 SSE:_mm_load / store与使用直接指针访问之间的区别


假设我想添加两个缓冲区并存储结果。两个缓冲区已经分配了16byte对齐。我找到了两个如何做到这一点的例子。

第一个是使用_mm_load将数据从缓冲区读入SSE寄存器,执行add操作并存储回结果寄存器。到现在为止,我会这样做。

void _add( uint16_t * dst, uint16_t const * src, size_t n )
{
  for( uint16_t const * end( dst + n ); dst != end; dst+=8, src+=8 )
  {
    __m128i _s = _mm_load_si128( (__m128i*) src );
    __m128i _d = _mm_load_si128( (__m128i*) dst );

    _d = _mm_add_epi16( _d, _s );

    _mm_store_si128( (__m128i*) dst, _d );
  }
}

第二个例子直接在内存地址上执行了添加操作,没有加载/存储操作。两缝都很好。

void _add( uint16_t * dst, uint16_t const * src, size_t n )
{
  for( uint16_t const * end( dst + n ); dst != end; dst+=8, src+=8 )
  {
    *(__m128i*) dst = _mm_add_epi16( *(__m128i*) dst, *(__m128i*) src );
  }
}

所以问题是第二个例子是否正确或可能有任何副作用,何时使用加载/存储是强制性的。

谢谢。


3041
2018-06-14 13:36


起源

有谁知道任何“官方”文件深入解释这个?我使用了“英特尔®C++内在函数参考”,但发现它没有清楚地回答我的问题。 - Peter
的主要目的 load/loadu 内在函数是将对齐信息传递给编译器。并且(对于float / double),从中进行类型转换 float* 至 __m128 要么 double* 至 __m128d。对于整数,你必须自己施展。 (但用AVX512修复,整数加载/存储内在函数占用 void* 参数) - Peter Cordes


答案:


两个版本都很好 - 如果你看一下生成的代码,你会发现第二个版本仍然会向向量寄存器生成至少一个加载,因为 PADDW (又名 _mm_add_epi16)只能直接从内存中获取第二个参数。

在实践中,大多数非平凡的SIMD代码将在加载和存储数据之间执行比单个添加更多的操作,因此通常您可能希望最初使用向量变量(寄存器)加载数据 _mm_load_XXX,对寄存器执行所有SIMD操作,然后将结果存储回存储器 _mm_store_XXX


10
2018-06-14 14:08



那么,你说的基本上是否有更多的操作可以重用我在第一个例子中可以保存的_d / _s变量,否则没有区别? - Peter
是的 - 几乎就是这样 - 加载和存储理想情况下应该是SIMD循环中相对较小的一部分(否则你很可能是内存带宽限制而不是计算限制)所以无关紧要 太 更确切地说,数据如何从内存到SIMD寄存器再返回。 - Paul R
@PaulR是否正确如果你使用load然后更改创建的变量源将不会改变,如果你使用指针并使更改源将改变? - Martinsos
@Martinsos:抱歉 - 我不完全明白你在问什么 - 也许你可以用一个代码示例发布一个新问题来说明你在问什么? - Paul R


主要区别在于,在第二个版本中,编译器将生成未对齐的载荷( movdqu 如果它不能证明指针是16字节对齐的话。根据周围的代码,甚至可能无法编写编译器可以证明此属性的代码。

否则没有区别,编译器足够聪明,可以将两个加载和一个加载加入到内存中,如果它认为有用,或者将加载和添加指令拆分为两个,则加载到内存中。

如果你使用的是c ++,你也可以写

void _add( __v8hi* dst, __v8hi const * src, size_t n )
{
    n /= 8;
    for( int i=0; i<n; ++i )
        d[i| += s[i];
}

__v8hi 是。的缩写 向量的8个半整数 要么 typedef short __v8hi __attribute__ ((__vector_size__ (16)));,每个矢量类型都有类似的预定义类型,由gcc和icc支持。

这将导致几乎相同的代码,这可能会或可能不会更快。但有人可能会说它更具可读性,可以很容易地扩展到AVX,甚至可能是编译器。


5
2018-06-15 07:23



我从来没有真正看到编译器为这种类型的转换生成错位负载。即使数据类型(故意)未对齐。当然,当我运行它时它崩溃了。 - Mysticial
我不止一次发生过这件事。 AFAIR涉及一些工会和铸造。 - Gunther Piez
我查看了代码的汇编,发现没有MOVDQU指令。一切都编译成MOVDQA所以它接缝很好。 - Peter
如果您想使用GNU C本机向量进行未对齐的加载/存储,则需要使用 __attribute__ ((__vector_size__ (16), aligned(1)))。看到 stackoverflow.com/questions/18199605/...。 gcc的emmintrin.h定义 __m128i 不使用 aligned(1)因此,假定取消引用指向它的指针是对齐的访问。 (它确实使用 __may_alias__但是,所以它被认为是别名,而不仅仅是 long long。) - Peter Cordes


至少有gcc / clang, foo = *dst; 与...完全相同 foo = _mm_load_si128(dst);。该 _mm_load_si128 方法通常是按照惯例首选的,但是对齐的C / C ++解除引用 __m128i* 也很安全。


的主要目的 load/loadu 内在函数是将对齐信息传递给编译器。

对于float / double,它们之间也是类型转换(constfloat* 和 __m128 要么 (constdouble* < - > __m128d。对于整数,你仍然需要自己投射:(。但是这已经被AVX512内在函数修复了,其中整数加载/存储内在函数采取 void* ARGS。

编译器仍然可以优化死存储或重新加载,并将加载折叠到ALU指令的存储器操作数中。但是当它们实际上在它们的汇编输出中发出存储或负载时,它们会以一种不会因为源中的对齐保证(或缺少)而出错的方式执行。

使用对齐的内在函数,编译器可以使用SSE或AVX将负载折叠到ALU指令的内存操作数中。但是未对齐的加载内在函数只能用AVX折叠,因为SSE内存操作数就像 movdqa 负载。例如 _mm_add_epi16(xmm0, _mm_loadu_si128(rax)) 可以编译成 vpaddw xmm0, xmm0, [rax] 使用AVX,但SSE必须编译到 movdqu xmm1, [rax] / paddw xmm0, xmm1。一个 load 代替 loadu 可以让它避免使用SSE单独的加载指令。


对于C来说是正常的,取消引用a __m128i* 被假定是一个对齐的访问,就像 load_si128 要么 store_si128

在gcc中 emmintrin.h__m128i type定义为 __attribute__ ((__vector_size__ (16), __may_alias__ ))

如果它已经使用过 __attribute__ ((__vector_size__ (16), __may_alias__, aligned(1) )),gcc会将取消引用视为未对齐访问。


1
2017-08-06 07:24



谢谢你的回答,但它是如此详细,如果我明白了,我就不会感到害羞。你是说两个版本都编译好了,但如果我不使用加载,编译器无法决定对齐并且总是假定未对齐的内存? - Peter
@Peter: foo = *dst 与...完全相同 foo = _mm_load_si128(dst)。取消引用时的“默认” __m128i 是一种可能在未对齐时出错的访问。 - Peter Cordes
我刚注意到的一件事是ICC 18.0正在使用 movdqu 即使我明确使用 _mm_load_si128 和 _mm_store_si128。 MSVC,GCC和CLANG仍然生成预期的对齐加载/存储指令。这是一个错误还是以英特尔的方式说“现在未对齐的加载/存储几乎没有影响所以我们只会一直使用未对齐的指令”? - user1593842
@ user1593842: movdqu 在对齐的地址上完全一样快 movdqa,在Nehalem和后来。 (以及AMD Bulldozer及其后)。 agner.org/optimize。 IDK为什么这样做;代码仍然可以在类似的东西上出错 paddd xmm0, [mem] 如果内存未对齐,ICC仍然会这样做。不过,MSVC做了同样的事情。也许他们只是将他们的asm输出函数简化为不关心对齐并始终使用未对齐版本。也许他们想要更宽容的不对齐?或者它可能是“瘫痪的AMD”功能; movdqu K10上的商店(不是负载)比较慢。 - Peter Cordes
@Peter Cordes:哦,我喜欢蹩脚的AMD理论:)“上次我们明确地创建了不同的代码路径,我们得到了很多不好的印象。好了,没有不同的代码路径了” - user1593842