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

时间:2012-06-14 13:36:52

标签: x86 sse simd

假设我想添加两个缓冲区并存储结果。两个缓冲区已经分配了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 );
  }
}

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

感谢。

3 个答案:

答案 0 :(得分:10)

两个版本都没问题 - 如果你看一下生成的代码,你会发现第二个版本仍然会向向量寄存器生成至少一个加载,因为PADDW(又名_mm_add_epi16)只能得到它的第二个参数直接来自记忆。

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

答案 1 :(得分:5)

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

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

如果您使用的是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,甚至可能被编译器扩展。

答案 2 :(得分:1)

至少使用gcc / clang,const res = fn(o) { A: { B: { C: [ {name: 'blah', value: 'vtha'} {name: 'vtha', value: 'blah'}, ] } } } foo = *dst;完全相同。按惯例,foo = _mm_load_si128(dst);方式通常是首选,但对齐_mm_load_si128的纯C / C ++解除引用也是安全的。

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

对于float / double,它们还在(loaduconstfloat*或(__m128const&lt; - &gt;之间进行类型转换。 double*。对于整数,你仍然需要自己投射:(。但是用AVX512内在函数修复了这个问题,其中整数加载/存储内在函数采用__m128d args。

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

使用对齐的内部函数,编译器可以将加载折叠到具有SSE或AVX的ALU指令的内存操作数中。但是未对齐的加载内在函数只能用AVX折叠,因为SSE内存操作数类似于void*加载。例如movdqa可以使用AVX编译为_mm_add_epi16(xmm0, _mm_loadu_si128(rax)),但SSE必须编译为vpaddw xmm0, xmm0, [rax] / movdqu xmm1, [rax]。使用paddw xmm0, xmm1代替load可以避免使用SSE单独加载指令。

与C一样正常,取消引用loadu被假定为对齐访问权限,例如__m128i*load_si128

在gcc&#39; store_si128中,emmintrin.h类型定义为__m128i

如果已使用__attribute__ ((__vector_size__ (16), __may_alias__ )),则gcc会将取消引用视为未对齐的访问权。