使用变量索引具有 _mm256_extract_epi32() 内在的 simd 向量
Using a variable to index a simd vector with _mm256_extract_epi32() intrinsic
我正在使用 AVX 内在函数 _mm256_extract_epi32()。
不过,我不完全确定我是否正确使用了它,因为 gcc 不喜欢我的代码,而 clang 编译并运行它没有问题。
我根据整数变量的值提取车道,而不是使用常量。
使用 clang3.8(或 clang4)为 avx2 编译以下代码片段时,它 generates code and uses the vpermd 指令。
#include <stdlib.h>
#include <immintrin.h>
#include <stdint.h>
uint32_t foo( int a, __m256i vec )
{
uint32_t e = _mm256_extract_epi32( vec, a );
return e*e;
}
现在,如果我改用 gcc,比方说 gcc 7.2,那么编译器将无法生成代码,并出现错误:
In file included from /opt/compiler-explorer/gcc-7.2.0/lib/gcc/x86_64-linux-gnu/7.2.0/include/immintrin.h:41:0,
from <source>:2:
/opt/compiler-explorer/gcc-7.2.0/lib/gcc/x86_64-linux-gnu/7.2.0/include/avxintrin.h: In function 'foo':
/opt/compiler-explorer/gcc-7.2.0/lib/gcc/x86_64-linux-gnu/7.2.0/include/avxintrin.h:524:20: error: the last argument must be a 1-bit immediate
return (__m128i) __builtin_ia32_vextractf128_si256 ((__v8si)__X, __N);
^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
In file included from /opt/compiler-explorer/gcc-7.2.0/lib/gcc/x86_64-linux-gnu/7.2.0/include/immintrin.h:37:0,
from <source>:2:
/opt/compiler-explorer/gcc-7.2.0/lib/gcc/x86_64-linux-gnu/7.2.0/include/smmintrin.h:449:11: error: selector must be an integer constant in the range 0..3
return __builtin_ia32_vec_ext_v4si ((__v4si)__X, __N);
^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
我有两个问题:
- 为什么 clang 可以使用变量,而 gcc 想要一个
常量?
- 为什么gcc拿不定主意?首先它需要一个 1 位立即值 ,然后它需要一个 0..3 范围内的整数常量,这些是不同的东西。
Intels Intrinsics Guide 没有指定 _mm256_extract_epi32() 的索引值的约束,所以谁是 gcc 或 clang?
显然 GCC 和 Clang 做出了不同的选择。
恕我直言,GCC 做出了正确的选择,没有为可变索引实现这一点。 Intrinsic _mm256_extract_epi32
不会转换为单个指令。使用可变索引,此内在函数可能会导致代码效率低下,
如果它用于性能关键循环。
例如,Clang 3.8 需要 4 条指令来实现具有可变索引的 _mm256_extract_epi32
。
GCC 迫使程序员考虑更高效的代码,避免使用可变索引 _mm256_extract_epi32
。
尽管如此,有时模拟 _mm256_extract_epi32
的可移植(gcc、clang、icc)函数还是很有用的
变量索引:
uint32_t mm256_extract_epi32_var_indx(const __m256i vec, const unsigned int i) {
__m128i indx = _mm_cvtsi32_si128(i);
__m256i val = _mm256_permutevar8x32_epi32(vec, _mm256_castsi128_si256(indx));
return _mm_cvtsi128_si32(_mm256_castsi256_si128(val));
}
内联后应该编译成三个指令:两个 vmovd
s 和一个 vpermd
(gcc 8.2 with -m64 -march=skylake -O3
):
mm256_extract_epi32_var_indx:
vmovd xmm1, edi
vpermd ymm0, ymm1, ymm0
vmovd eax, xmm0
vzeroupper
ret
请注意,内在函数指南描述了索引的结果为 0
>=8(无论如何这是一个不寻常的情况)。在 Clang 3.8 和 mm256_extract_epi32_var_indx
中,索引以 8 为模减少。换句话说:仅使用索引的 3 个最低有效位。
请注意,Clang 5.0 的内存往返也不是很有效,
参见 this Godbolt link。叮当声 7.0
无法使用变量索引编译 _mm256_extract_epi32
。
As :使用固定索引 0、1、2 或 3,只需要一条 pextrd
指令即可
从 xmm 寄存器中提取整数。对于固定索引 4、5、6 或 7,需要两条指令。
不幸的是,不存在用于 256 位 ymm 寄存器的 vpextrd
指令。
下一个例子说明了我的答案:
从 SIMD 内在函数开始的天真的程序员可能会写
以下代码将元素 0、1、...、j-1 与 j<8
相加,来自 vec
.
#include <stdlib.h>
#include <immintrin.h>
#include <stdint.h>
uint32_t foo( __m256i vec , int j)
{
uint32_t sum=0;
for (int i = 0; i < j; i++){
sum = sum + (uint32_t)_mm256_extract_epi32( vec, i );
}
return sum;
}
在 Clang 3.8 中,这编译为大约 50 instructions 分支和循环。 GCC 无法编译此代码。
显然,对这些元素求和的有效代码可能基于:
- 屏蔽掉元素 j、j+1、...、7 和
- 计算水平总和。
它说的 __N
必须是 1 位立即数是 而不是 _mm256_extract_epi32
的第二个参数,它是用作arg 到 __builtin_ia32_vextractf128_si256
(大概是第 3 位)。然后稍后它想要一个整数常量在 0..3 范围内 vpextrd
,给你总共 3 位索引。
_mm256_extract_epi32
是复合内在函数,不是根据单个 builtin
函数直接定义的。
vpextrd r32, ymm, imm8
不存在,只有 xmm 版本存在,所以 _mm256_extract_epi32
是 vextracti/f128
/ vpextrd
的包装器。 Gcc 选择仅使其适用于编译时常量,因此它始终最多编译为 2 条指令。
如果你想要运行时变量向量索引,你需要使用不同的语法;例如存储到一个数组并加载一个标量,并希望 gcc 将其优化为随机播放/提取。
或者定义一个具有正确元素宽度的 GNU C 本机向量类型,并使用 foo[i]
像数组一样对其进行索引。
typedef int v8si __attribute__ ((vector_size (32)));
v8si tmp = foo; // may need a cast to convert from __m256i
int element_i = tmp[i];
gcc/clang中的__m256i
被定义为long long
个元素的向量,所以如果你直接用[]
索引它,你会得到qword元素。 (并且您的代码无法使用 MSVC 进行编译,MSVC 根本没有以这种方式定义 __m256i
。)
我最近没有检查 asm 中的任何一个:如果你关心效率,你可能想使用你的运行时变量索引手动设计一个 shuffle,就像@Wim 的回答表明 clang 那样。
我正在使用 AVX 内在函数 _mm256_extract_epi32()。
不过,我不完全确定我是否正确使用了它,因为 gcc 不喜欢我的代码,而 clang 编译并运行它没有问题。
我根据整数变量的值提取车道,而不是使用常量。
使用 clang3.8(或 clang4)为 avx2 编译以下代码片段时,它 generates code and uses the vpermd 指令。
#include <stdlib.h>
#include <immintrin.h>
#include <stdint.h>
uint32_t foo( int a, __m256i vec )
{
uint32_t e = _mm256_extract_epi32( vec, a );
return e*e;
}
现在,如果我改用 gcc,比方说 gcc 7.2,那么编译器将无法生成代码,并出现错误:
In file included from /opt/compiler-explorer/gcc-7.2.0/lib/gcc/x86_64-linux-gnu/7.2.0/include/immintrin.h:41:0,
from <source>:2:
/opt/compiler-explorer/gcc-7.2.0/lib/gcc/x86_64-linux-gnu/7.2.0/include/avxintrin.h: In function 'foo':
/opt/compiler-explorer/gcc-7.2.0/lib/gcc/x86_64-linux-gnu/7.2.0/include/avxintrin.h:524:20: error: the last argument must be a 1-bit immediate
return (__m128i) __builtin_ia32_vextractf128_si256 ((__v8si)__X, __N);
^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
In file included from /opt/compiler-explorer/gcc-7.2.0/lib/gcc/x86_64-linux-gnu/7.2.0/include/immintrin.h:37:0,
from <source>:2:
/opt/compiler-explorer/gcc-7.2.0/lib/gcc/x86_64-linux-gnu/7.2.0/include/smmintrin.h:449:11: error: selector must be an integer constant in the range 0..3
return __builtin_ia32_vec_ext_v4si ((__v4si)__X, __N);
^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
我有两个问题:
- 为什么 clang 可以使用变量,而 gcc 想要一个 常量?
- 为什么gcc拿不定主意?首先它需要一个 1 位立即值 ,然后它需要一个 0..3 范围内的整数常量,这些是不同的东西。
Intels Intrinsics Guide 没有指定 _mm256_extract_epi32() 的索引值的约束,所以谁是 gcc 或 clang?
显然 GCC 和 Clang 做出了不同的选择。
恕我直言,GCC 做出了正确的选择,没有为可变索引实现这一点。 Intrinsic _mm256_extract_epi32
不会转换为单个指令。使用可变索引,此内在函数可能会导致代码效率低下,
如果它用于性能关键循环。
例如,Clang 3.8 需要 4 条指令来实现具有可变索引的 _mm256_extract_epi32
。
GCC 迫使程序员考虑更高效的代码,避免使用可变索引 _mm256_extract_epi32
。
尽管如此,有时模拟 _mm256_extract_epi32
的可移植(gcc、clang、icc)函数还是很有用的
变量索引:
uint32_t mm256_extract_epi32_var_indx(const __m256i vec, const unsigned int i) {
__m128i indx = _mm_cvtsi32_si128(i);
__m256i val = _mm256_permutevar8x32_epi32(vec, _mm256_castsi128_si256(indx));
return _mm_cvtsi128_si32(_mm256_castsi256_si128(val));
}
内联后应该编译成三个指令:两个 vmovd
s 和一个 vpermd
(gcc 8.2 with -m64 -march=skylake -O3
):
mm256_extract_epi32_var_indx:
vmovd xmm1, edi
vpermd ymm0, ymm1, ymm0
vmovd eax, xmm0
vzeroupper
ret
请注意,内在函数指南描述了索引的结果为 0
>=8(无论如何这是一个不寻常的情况)。在 Clang 3.8 和 mm256_extract_epi32_var_indx
中,索引以 8 为模减少。换句话说:仅使用索引的 3 个最低有效位。
请注意,Clang 5.0 的内存往返也不是很有效,
参见 this Godbolt link。叮当声 7.0
无法使用变量索引编译 _mm256_extract_epi32
。
As pextrd
指令即可
从 xmm 寄存器中提取整数。对于固定索引 4、5、6 或 7,需要两条指令。
不幸的是,不存在用于 256 位 ymm 寄存器的 vpextrd
指令。
下一个例子说明了我的答案:
从 SIMD 内在函数开始的天真的程序员可能会写
以下代码将元素 0、1、...、j-1 与 j<8
相加,来自 vec
.
#include <stdlib.h>
#include <immintrin.h>
#include <stdint.h>
uint32_t foo( __m256i vec , int j)
{
uint32_t sum=0;
for (int i = 0; i < j; i++){
sum = sum + (uint32_t)_mm256_extract_epi32( vec, i );
}
return sum;
}
在 Clang 3.8 中,这编译为大约 50 instructions 分支和循环。 GCC 无法编译此代码。 显然,对这些元素求和的有效代码可能基于:
- 屏蔽掉元素 j、j+1、...、7 和
- 计算水平总和。
它说的 __N
必须是 1 位立即数是 而不是 _mm256_extract_epi32
的第二个参数,它是用作arg 到 __builtin_ia32_vextractf128_si256
(大概是第 3 位)。然后稍后它想要一个整数常量在 0..3 范围内 vpextrd
,给你总共 3 位索引。
_mm256_extract_epi32
是复合内在函数,不是根据单个 builtin
函数直接定义的。
vpextrd r32, ymm, imm8
不存在,只有 xmm 版本存在,所以 _mm256_extract_epi32
是 vextracti/f128
/ vpextrd
的包装器。 Gcc 选择仅使其适用于编译时常量,因此它始终最多编译为 2 条指令。
如果你想要运行时变量向量索引,你需要使用不同的语法;例如存储到一个数组并加载一个标量,并希望 gcc 将其优化为随机播放/提取。
或者定义一个具有正确元素宽度的 GNU C 本机向量类型,并使用 foo[i]
像数组一样对其进行索引。
typedef int v8si __attribute__ ((vector_size (32)));
v8si tmp = foo; // may need a cast to convert from __m256i
int element_i = tmp[i];
gcc/clang中的__m256i
被定义为long long
个元素的向量,所以如果你直接用[]
索引它,你会得到qword元素。 (并且您的代码无法使用 MSVC 进行编译,MSVC 根本没有以这种方式定义 __m256i
。)
我最近没有检查 asm 中的任何一个:如果你关心效率,你可能想使用你的运行时变量索引手动设计一个 shuffle,就像@Wim 的回答表明 clang 那样。