为啥“+=”会给我在 SSE 内在的意外结果

Posted

技术标签:

【中文标题】为啥“+=”会给我在 SSE 内在的意外结果【英文标题】:why does "+=" gives me unexpected result in SSE instrinsic为什么“+=”会给我在 SSE 内在的意外结果 【发布时间】:2019-06-13 01:50:43 【问题描述】:

sse intrinsic 有两种实现累加的方式。但是其中一个得到了错误的结果。

#include <smmintrin.h>

int main(int argc, const char * argv[]) 

int32_t A[4] = 10, 20, 30, 40;
int32_t B[8] = -1, 2, -3, -4, -5, -6, -7, -8;
int32_t C[4] = 0, 0, 0, 0;
int32_t D[4] = 0, 0, 0, 0;

__m128i lv = _mm_load_si128((__m128i *)A);
__m128i rv = _mm_load_si128((__m128i *)B);

// way 1 unexpected
rv += lv;
_mm_store_si128((__m128i *)C, rv);

// way 2 expected
rv = _mm_load_si128((__m128i *)B);
rv = _mm_add_epi32(lv, rv);
_mm_store_si128((__m128i *)D, rv);

return 0;

预期结果是:

9 22 27 36

C 是:

9 23 27 37

D 是:

9 22 27 36

【问题讨论】:

【参考方案1】:

在 GNU C 中,__m128i 被定义为 64 位 整数的向量,类似于

typedef long long __m128i __attribute__((vector_size(16), may_alias));

使用 GNU C 原生向量语法(+ 运算符)以 64 位元素大小进行逐元素相加。即_mm_add_epi64

在您的情况下,从一个 32 位元素顶部的进位向其上方的 32 位元素添加了一个额外的 1,因为 64 位元素大小确实会在成对的 32 位元素之间传播进位。 (将负数添加到非零目标会产生结转。)


英特尔内部 API 没有为 __m128 / __m128d / __m128i 定义 + 运算符。例如,您的代码不会在 MSVC 上编译。

因此,您得到的行为仅来自 GCC 标头中内在类型的实现细节。它对于元素大小明显的浮点向量很有用,但对于整数向量,您需要自己定义,除非您碰巧有 64 位整数。


如果您希望能够使用v1 += v2;,您可以定义自己的 GNU C 本机向量类型,例如

typedef uint32_t v4ui __attribute__((vector_size(16), aligned(4)));

请注意,我省略了may_alias,因此只有将指针转换为unsigned 是安全的,而不是读取char[] 之类的任意数据。

事实上 GCC 的 emmintrin.h (SSE2) 确实定义了一堆类型:

/* SSE2 */
typedef double __v2df __attribute__ ((__vector_size__ (16)));
typedef long long __v2di __attribute__ ((__vector_size__ (16)));
typedef unsigned long long __v2du __attribute__ ((__vector_size__ (16)));
typedef int __v4si __attribute__ ((__vector_size__ (16)));
typedef unsigned int __v4su __attribute__ ((__vector_size__ (16)));
typedef short __v8hi __attribute__ ((__vector_size__ (16)));
typedef unsigned short __v8hu __attribute__ ((__vector_size__ (16)));
typedef char __v16qi __attribute__ ((__vector_size__ (16)));
typedef unsigned char __v16qu __attribute__ ((__vector_size__ (16)));

我不确定它们是否适合外用。

当您想让编译器发出有效的代码以除以编译时常量或类似的东西时,GNU C 本机向量是最有用的。例如带有 16 位无符号整数的 digit = v1 % 10;v1 /= 10; 将编译为 pmulhuw 和右移。但它们对于可读代码也很方便。


有一些 C++ 包装库可移植地提供运算符重载,并且具有像 Vec4i (4x signed int) / Vec4u (4x unsigned int) / Vec16c (16x signed char) 这样的类型来给你一个类型用于不同类型整数向量的系统,因此您知道您从 v1 += v2;v1 &gt;&gt;= 2; 得到什么(右移是符号很重要的一种情况。)

例如Agner Fog 的 VCL(GPL 许可)或 DirectXMath(MIT 许可)。

【讨论】:

以上是关于为啥“+=”会给我在 SSE 内在的意外结果的主要内容,如果未能解决你的问题,请参考以下文章

VS:_BitScanReverse64内在的意外优化行为

为啥此代码片段返回意外结果?

为啥我在 React JS 中遇到意外错误 [重复]

为啥意外的无限循环会增加 CPU 使用率?

为啥使用 Pandas 的 max 和 min 函数会返回意外结果?

按位运算的意外结果为啥 (1 | 1 & 2) 给出 1 而不是 2?