【问题标题】:SSE: Difference between _mm_load/store vs. using direct pointer accessSSE:_mm_load/store 与使用直接指针访问之间的区别
【发布时间】:2012-06-17 13:22:28
【问题描述】:

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

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

谢谢。

【问题讨论】:

  • 有谁知道任何“官方”文件深入解释这一点?我使用了“Intel® C++ Intrinsics Reference”,但发现它不能清楚地回答我的问题。
  • load/loadu 内部函数的主要目的是将对齐信息传递给编译器。并且(对于浮点数/双精度数),从float*__m128double*__m128d 的类型转换。对于整数,您必须自己转换。 (但使用 AVX512 修复,其中整数加载/存储内在函数采用 void* args)

标签: x86 sse simd


【解决方案1】:

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

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

【讨论】:

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

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

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

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

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

__v8hivector of 8 half integerstypedef short __v8hi __attribute__ ((__vector_size__ (16)));的缩写,每个vector类型都有类似的预定义类型,gcc和icc都支持。

这将产生几乎相同的代码,可能会更快,也可能不会更快。但有人可能会争辩说它更具可读性,并且可以很容易地扩展到 AVX,甚至可能通过编译器。

【讨论】:

  • 我实际上从未见过编译器为这种类型的转换生成未对齐的负载。即使数据类型(故意)未对齐。当然,当我运行它时它会崩溃。
  • 我不止一次遇到过这种情况。 AFAIR 涉及一些工会和选角。
  • 我查看了我的代码汇编,发现没有 MOVDQU 指令。一切都编译为 MOVDQA,所以它看起来很好。
  • 如果你想使用 GNU C 原生向量进行未对齐的加载/存储,你需要使用__attribute__ ((__vector_size__ (16), aligned(1)))。见stackoverflow.com/questions/18199605/…。 gcc 的 __m128i 的 emmintrin.h 定义不使用 aligned(1),因此取消引用指向它的指针被假定为对齐访问。 (不过,它确实使用了__may_alias__,所以它被假定为任何东西,而不仅仅是long long。)
【解决方案3】:

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


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

对于 float/double,它们还在 (const) float*__m128 或 (const) double* __m128d 之间进行类型转换。对于整数,您仍然必须自己转换:(。但这已通过 AVX512 内在函数修复,其中整数加载/存储内在函数采用 void* args。

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

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


与 C 的正常情况一样,取消引用 __m128i* 被假定为对齐访问,例如 load_si128store_si128

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

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

【讨论】:

  • 感谢您的回答,但它是如此详细,如果我明白了这一点,我不知道。你是说两个版本都可以编译,但是如果我不使用 load 编译器就无法决定对齐并且总是假设内存未对齐?
  • @Peter:foo = *dstfoo = _mm_load_si128(dst) 完全相同。取消引用 __m128i 时的“默认”是一种可能在未对齐时出错的访问。
  • 我刚刚注意到的一件事是,即使我明确使用 _mm_load_si128_mm_store_si128,ICC 18.0 仍在使用 movdqu。 MSVC、GCC 和 CLANG 仍会生成预期的对齐加载/存储指令。这是一个错误还是英特尔所说的“现在未对齐的加载/存储几乎没有影响,所以我们将一直简单地使用未对齐的指令”?
  • @user1593842: movdqu 在对齐的地址上与movdqa 在 Nehalem 及更高版本上的速度完全相同。 (以及在 AMD Bulldozer 及更高版本上)。 agner.org/optimize。 IDK 他们为什么这样做;如果内存未对齐,代码仍然会出现 paddd xmm0, [mem] 之类的错误,而 ICC 仍然会这样做。不过,MSVC 做同样的事情。也许他们只是简化了他们的 asm 输出功能,不关心对齐,总是使用未对齐的版本。也许他们想更宽容不结盟?或者它可能是一个“残废的 AMD”功能; movdqu 存储(不是加载)在 K10 上较慢。
  • @Peter Cordes:哦,我喜欢残废的 AMD 理论 :)“上次我们明确创建不同的代码路径时,我们得到了很多不好的报道。好吧,不再有不同的代码路径了”
猜你喜欢
  • 2014-03-25
  • 2019-08-11
  • 1970-01-01
  • 1970-01-01
  • 2010-11-22
  • 1970-01-01
  • 2020-06-07
  • 1970-01-01
  • 2010-12-26
相关资源
最近更新 更多