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

Pet*_*ter 16 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 );
  }
}
Run Code Online (Sandbox Code Playgroud)

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

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

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

谢谢.

Pau*_*l R 11

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

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


Gun*_*iez 6

主要区别在于,在第二个版本中,如果编译器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];
}
Run Code Online (Sandbox Code Playgroud)

__v8hi8个半整数的向量的缩写,或者typedef short __v8hi __attribute__ ((__vector_size__ (16)));每种向量类型都有类似的预定义类型,由gcc和icc支持.

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