X86 SSE:_mm_加载/存储与使用直接指针访问之间的差异

X86 SSE:_mm_加载/存储与使用直接指针访问之间的差异,x86,sse,simd,X86,Sse,Simd,假设我想添加两个缓冲区并存储结果。两个缓冲区都已分配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 ) { __m128

假设我想添加两个缓冲区并存储结果。两个缓冲区都已分配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 );
  }
}
第二个示例只是直接对内存地址执行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*) dst = _mm_add_epi16( *(__m128i*) dst, *(__m128i*) src );
  }
}
因此,问题是第二个示例是否正确或可能有任何副作用,以及何时使用加载/存储是必需的


谢谢

两个版本都很好-如果查看生成的代码,您会发现第二个版本仍然会生成至少一个向量寄存器的加载,因为
PADDW
(又称
\u mm\u add\u epi16
)只能直接从内存中获取第二个参数


在实践中,大多数非平凡的SIMD代码在加载和存储数据之间所做的操作比单个加法要多得多,因此一般来说,您可能希望使用
\u mm\u load\u XXX
将数据最初加载到向量变量(寄存器),在寄存器上执行所有SIMD操作,然后通过
\u mm\u store\u XXX

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

否则就没有区别了,编译器足够聪明,可以将两个加载和add转换成一个加载,如果它认为有用,还可以从内存中进行add,或者将一个加载拆分成两个并添加指令

如果你使用C++,你也可以写< /P>

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

对于(int i=0;i至少带有gcc/clang,
foo=*dst;
foo=\u mm\u load\u si128(dst);
完全相同。
\u mm\u load\u si128
方式通常是约定的首选方式,但对对齐的
\u m128i*
进行简单的C/C++解引用也是安全的



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

对于float/double,它们还可以在(
const
float*
\uuum128
或(
const
double*
\uuuum128d
之间进行强制转换。对于integer,您仍然必须强制转换自己:(。但这在AVX512内部函数中是固定的,整数加载/存储内部函数取
void*/code>参数

编译器仍然可以优化死区存储或重新加载,并将加载折叠到ALU指令的内存操作数中。但是,当它们确实在其程序集输出中发出存储或加载时,如果源代码中有对齐保证(或缺少对齐保证),它们会以一种不会出错的方式执行

使用对齐的内部函数可以让编译器将加载折叠到带有SSE或AVX的ALU指令的内存操作数中。但未对齐的加载内部函数只能与AVX折叠,因为SSE内存操作数类似于
movdqa
loads。例如
\u mm\u add\u epi16(xmm0,\u mm\u loadu\u si128(rax))
可以编译到
vpaddw xmm0,xmm0,[rax]
使用AVX,但使用SSE必须编译为
movdqu xmm1,[rax]
/
paddw xmm0,xmm1
。使用
load
而不是
loadu
也可以避免使用SSE执行单独的加载指令


与C一样,对
\uuum128i*
的解引用被认为是一个对齐的访问,如
加载
存储

在gcc的
emmintrin.h
中,
\uuuuum128i
类型是用
\uuuuuuuu属性(((uuuu向量大小uuuuuu16),\uu可以uu别名uuuuuuu))定义的。


如果它使用了
\uuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuu
,gcc会将取消引用视为未对齐的访问。

因此,您所说的基本上是,如果我有更多的操作,可以重用第一个示例中可以保存的_d/_s变量,否则就没有什么区别了?是的,这差不多是它-加载和存储在理想情况下应该是SIMD循环中相对较小的一部分(否则,很可能是内存带宽限制,而不是计算限制)因此,数据从内存到SIMD寄存器再到SIMD寄存器的确切方式并不重要。@PaulR如果使用load然后更改,则创建的变量源不会更改,而如果使用pointer并进行更改,则源会更改,这是正确的吗?@Martinsos:抱歉-我不完全理解您的要求-也许您可以发布一个用一个代码示例来说明你在问什么?新的问题有人知道有什么“官方”文档在深入地解释这一点吗?我使用了“英特尔®C++内部内容引用”。,但没有明确回答我的问题。
load
/
loadu
内部函数的主要目的是将对齐信息传递给编译器。And(对于float/double),要从
float*
\uuum128
double*
\uuuum128d
进行强制转换,您必须自己强制转换整数。(但使用AVX512修复了此问题,整数加载/存储内部函数采用
void*
参数)我从未见过编译器为这种类型的转换生成未对齐的负载未对齐。当然,当我运行它时,它会崩溃。我不止一次遇到这种情况。AFAIR涉及到一些联合和转换。我查看了我的代码汇编,没有发现MOVDQU指令。所有内容都编译到MOVDQA,所以它很好。如果你想使用GNU C本机向量进行未对齐的加载/存储,你需要使用
\uuuuu属性