为什么“+=”在 SSE intrinsic 中给我意想不到的结果
why does "+=" gives me unexpected result in SSE instrinsic
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
在 GNU C 中,__m128i
被定义为 64 位 整数向量,类似于
typedef long long __m128i __attribute__((vector_size(16), may_alias));
使用 GNU C 本机向量语法(+
运算符)执行 per-element 64 位元素大小的加法。即 _mm_add_epi64
.
在您的例子中,carry-out 从一个 32 位元素的顶部向其上方的 32 位元素添加了一个额外的元素,因为 64 位元素大小确实在 32 位元素对之间传播进位-位元素。 (将负数添加到 non-zero 目的地会产生 carry-out。)
英特尔内部函数 API 没有为 __m128
/ __m128d
/ __m128i
定义 +
运算符。例如,您的代码无法在 MSVC 上编译。
所以您得到的行为仅来自 GCC headers 中内在类型的实现细节。它对于具有明显元素大小的浮点向量很有用,但对于整数向量,您需要自己定义,除非您碰巧有 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 本机向量在您想让编译器生成除以 compile-time 常量或类似内容的高效代码时最有用。例如具有 16 位无符号整数的 digit = v1 % 10;
和 v1 /= 10;
将编译为 pmulhuw
和右移。但它们对于可读代码也很方便。
有一些 C++ 包装器库可移植地提供运算符重载,并且具有像 Vec4i
(4x signed int) / Vec4u
(4x unsigned int) / Vec16c
(16x signed char) 为不同类型的整数向量提供类型系统,因此您知道从 v1 += v2;
或 v1 >>= 2;
得到什么(右移是符号重要的一种情况。)
例如Agner Fog 的 VCL(GPL 许可证)或 DirectXMath(MIT 许可证)。
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
在 GNU C 中,__m128i
被定义为 64 位 整数向量,类似于
typedef long long __m128i __attribute__((vector_size(16), may_alias));
使用 GNU C 本机向量语法(+
运算符)执行 per-element 64 位元素大小的加法。即 _mm_add_epi64
.
在您的例子中,carry-out 从一个 32 位元素的顶部向其上方的 32 位元素添加了一个额外的元素,因为 64 位元素大小确实在 32 位元素对之间传播进位-位元素。 (将负数添加到 non-zero 目的地会产生 carry-out。)
英特尔内部函数 API 没有为 __m128
/ __m128d
/ __m128i
定义 +
运算符。例如,您的代码无法在 MSVC 上编译。
所以您得到的行为仅来自 GCC headers 中内在类型的实现细节。它对于具有明显元素大小的浮点向量很有用,但对于整数向量,您需要自己定义,除非您碰巧有 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 本机向量在您想让编译器生成除以 compile-time 常量或类似内容的高效代码时最有用。例如具有 16 位无符号整数的 digit = v1 % 10;
和 v1 /= 10;
将编译为 pmulhuw
和右移。但它们对于可读代码也很方便。
有一些 C++ 包装器库可移植地提供运算符重载,并且具有像 Vec4i
(4x signed int) / Vec4u
(4x unsigned int) / Vec16c
(16x signed char) 为不同类型的整数向量提供类型系统,因此您知道从 v1 += v2;
或 v1 >>= 2;
得到什么(右移是符号重要的一种情况。)
例如Agner Fog 的 VCL(GPL 许可证)或 DirectXMath(MIT 许可证)。