19

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

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

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 );
  }
}

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

谢谢。

4

3 回答 3

13

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

在实践中,大多数重要的 SIMD 代码将在加载和存储数据之间执行更多操作,而不仅仅是一次添加,因此通常您可能希望最初使用 将数据加载到向量变量(寄存器)_mm_load_XXX,在寄存器上执行所有 SIMD 操作,然后通过 将结果存储回内存_mm_store_XXX

于 2012-06-14T14:08:31.717 回答
7

主要区别在于,在第二个版本中,如果编译器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,甚至可能通过编译器。

于 2012-06-15T07:23:40.467 回答
2

至少使用 gcc/clang,foo = *dst;foo = _mm_load_si128(dst);. 按照惯例,这种_mm_load_si128方式通常是首选的,但是对对齐的普通 C/C++ 取消引用__m128i*也是安全的。


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

对于 float/double,它们还在 ( const)float*__m128or ( const) double*<->之间进行类型转换__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。Aload而不是也loadu可以让它避免使用 SSE 的单独加载指令。


与 C 一样,取消引用 a__m128i*被假定为对齐访问,例如load_si128or store_si128

在 gcc 中emmintrin.h__m128i类型是用 定义的__attribute__ ((__vector_size__ (16), __may_alias__ ))

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

于 2017-08-06T07:24:48.897 回答