在运行时从 simd 寄存器获取任意浮点数?
Get an arbitrary float from a simd register at runtime?
我想从 simd 寄存器访问任意浮点数。我知道我可以做这样的事情:
float get(const __m128i& a, const int idx){
// editor's note: this type-puns the FP bit-pattern to int and converts to float
return _mm_extract_ps(a,idx);
}
或
float get(const __m128i& a, const int idx){
return _mm_cvtss_f32(_mm_shuffle_ps(a,_MM_SHUFFLE(0,0,0,idx));
}
甚至使用轮班而不是随机播放。问题是这些都需要在编译时知道 idx(shuffle、shift 和 extract 都需要 8 位立即数)。
我也可以使用 _mm_store_ps()
然后使用生成的数组来完成它,但这需要内存。有没有比这更快的方法?
编辑:忽略第一个代码片段,我想要那个位置的浮点数,而不是像 _mm_extract_ps
returns.
这样的整数
首先你肯定不想要_mm_extract_ps
,除非你想把FP打成int
1.
但是无论如何,对于运行时变量索引,您可能不想分支到 select 具有正确 imm8 的指令。
source + asm output for gcc/icc/clang/msvc on the Godbolt compiler explorer 此答案中的所有功能。 包括(在底部)一些使用编译时常量 idx 的测试调用程序,这样您就可以看到在实际程序中发生内联 + 常量传播时会发生什么。 And/or 来自同一向量的两个索引(仅 gcc CSE 和从同一存储重新加载两次,其他编译器存储两次)。
Store/reload 使用 gcc/clang/ICC 进行优化(但可变 idx 版本的延迟更高)。 其他方法只能很好地优化带有 clang 的常量输入。 (clang 甚至可以看穿 pshufb
版本并将其变成 vshufps imm8
或 vpermilps imm8
,或者 idx=0 的空操作)。其他编译器做一些愚蠢的事情,比如用 vxorps
将向量归零并将其用作 vpermilps
控件!
128 位向量:如果您有 SSSE3 pshufb
或 AVX
,请使用变量洗牌
使用 AVX1,对于 128 位向量,使用 vpermilps
,您只需 2 个 ALU 微指令即可完成,这是一个使用双字 [=203] 的变量洗牌=] 或元素,不像 pshufb
.
这使您可以执行与 _mm_shuffle_ps
完全相同的随机播放(包括将低位元素复制到高位 3 个元素,这很好),但使用运行时索引而不是立即数。
// you can pass vectors by value. Not that it matters when inlining
static inline
float get128_avx(__m128i a, int idx){
__m128i vidx = _mm_cvtsi32_si128(idx); // vmovd
__m128 shuffled = _mm_permutevar_ps(a, vidx); // vpermilps
return _mm_cvtss_f32(shuffled);
}
gcc 和 clang 像这样为 x86-64 编译它(Godbolt 编译器资源管理器):
vmovd xmm1, edi
vpermilps xmm0, xmm0, xmm1
ret
没有 AVX 但有 SSSE3,您可以为 pshufb
加载或创建掩码。索引 4 个 __m128i
向量的数组是相当常见的,尤其是使用 _mm_movemask_ps
结果作为索引时。但是这里我们只关心低32位的元素,所以我们可以做得更好。
事实上,模式的常规性质意味着我们可以使用两个 32 位立即数操作数通过乘法和加法来创建它。
static inline
float get128_ssse3(__m128 a, int idx) {
const uint32_t low4 = 0x03020100, step4=0x04040404;
uint32_t selector = low4 + idx*step4;
__m128i vidx = _mm_cvtsi32_si128(selector);
// alternative: load a 4-byte window into 0..15 from memory. worse latency
// static constexpr uint32_t shuffles[4] = { low4, low4+step4*1, low4+step4*2, low4+step4*3 };
//__m128i vidx = _mm_cvtsi32_si128(shuffles[idx]);
__m128i shuffled = _mm_shuffle_epi8(_mm_castps_si128(a), vidx);
return _mm_cvtss_f32(_mm_castsi128_ps(shuffled));
}
-O3 -march=nehalem
的 gcc 输出(其他编译器也这样做,模块可能被浪费了 movaps
):
get128_ssse3(float __vector(4), int):
imul edi, edi, 67372036 # 0x04040404
add edi, 50462976 # 0x03020100
movd xmm1, edi
pshufb xmm0, xmm1
ret # with the float we want at the bottom of XMM0
因此,如果没有 AVX,store/reload 可以节省指令(和 uops),特别是如果编译器可以避免对索引进行符号扩展或零扩展。
从 idx 到结果的延迟 = imul(3) + add(1) + movd(2) + pshufb(1) 在来自 Core2(Penryn) 和更新版本的 Intel CPU 上。不过,从输入向量到结果的延迟仅为 pshufb
。 (加上 Nehalem 上的旁路延迟延迟。)http://agner.org/optimize/
__m256
256 位向量:使用 AVX2 随机播放,否则可能 store/reload
与 AVX1 不同,AVX2 具有像 vpermps
这样的跨车道变量随机播放。 (AVX1 仅对整个 128 位通道进行立即洗牌。)我们可以使用 vpermps
作为 AVX1 vpermilps
的直接替代,从 256 位向量中获取元素。
vpermps
有两个内部函数(参见 Intel's intrinsics finder)。
_mm256_permutevar8x32_ps(__m256 a, __m256i idx)
:旧名称,操作数与 asm 指令相反。
_mm256_permutexvar_ps(__m256i idx, __m256 a)
:新名称,随 AVX512 引入,操作数顺序正确(匹配 asm 操作数顺序,与 _mm_shuffle_epi8
或 _mm_permutevar_ps
相反). asm instruction-set reference manual entry只列出了这个版本,并且列出了错误的类型(__m256 i
用于控制操作数)。
gcc 和 ICC 在仅启用 AVX2 而不是 AVX512 的情况下接受此助记符。但不幸的是,clang 只接受 -mavx512vl
(或 -march=skylake-avx512
),所以你不能便携地使用它。因此,只需使用笨拙的 8x32 名称,它在任何地方都适用。
#ifdef __AVX2__
float get256_avx2(__m256 a, int idx) {
__m128i vidx = _mm_cvtsi32_si128(idx); // vmovd
__m256i vidx256 = _mm256_castsi128_si256(vidx); // no instructions
__m256 shuffled = _mm256_permutevar8x32_ps(a, vidx256); // vpermps
return _mm256_cvtss_f32(shuffled);
}
// operand order matches asm for the new name: index first, unlike pshufb and vpermilps
//__m256 shuffled = _mm256_permutexvar_ps(vidx256, a); // vpermps
#endif
_mm256_castsi128_si256
从技术上讲并没有留下上层通道未定义(因此编译器永远不需要花费指令零扩展),但无论如何我们不关心上层通道。
这编译为
vmovd xmm1, edi
vpermps ymm0, ymm1, ymm0
# vzeroupper # these go away when inlining
# ret
所以它在 Intel CPU 上非常棒,从输入向量到结果的延迟只有 3c,吞吐量成本为 2 uops(但两个 uops 都需要端口 5)。
AMD 上的车道交叉洗牌要贵得多。
Store/reload
store/reload实际上很好的情况:
- 不带 AVX2 的 256 位向量,或不带 SSSE3 的 128 位向量。
- 如果您需要同一向量中的 2 个或更多元素(但请注意,如果您实际调用
get128_reload
,gcc 以外的编译器会存储多次。因此,如果您这样做,手动内联向量存储并对其进行多次索引。)
当 ALU 端口压力(尤其是 shuffle 端口)成为问题时,吞吐量比延迟更重要。在 Intel CPU 上,movd xmm, eax
也在端口 5 上运行,因此它与 shuffle 竞争。但希望你只在内部循环之外使用标量提取,周围有很多代码可以做其他事情。
当 idx
通常是编译时常量并且您希望让编译器为您选择随机播放时。
一个糟糕的 idx
可能会使您的程序崩溃,而不仅仅是给您错误的元素。 将索引直接转换为随机播放控件的方法会忽略高位。
注意。在 Godbolt 示例中,ICC 可以使用 test_reload2
。
Store/reload 到本地数组对于吞吐量来说完全没问题(可能不是延迟),并且在典型的 CPU 上只有大约 6 个周期的延迟,这要归功于存储-转发。大多数 CPU 的前端吞吐量比矢量 ALU 多,因此如果您在 ALU 吞吐量而不是 store/load 吞吐量上接近瓶颈,那么在混合中加入一些 store/reload 一点也不坏。
宽存储可以转发到窄重新加载,但要遵守一些对齐约束。我认为在主流 Intel CPU 上,矢量的 4 或 8 个元素中的任何一个自然对齐的 dword 重新加载都很好,但您可以查看 Intel 的优化手册。请参阅 the x86 tag wiki 中的性能链接。
在 GNU C 中,您可以像数组一样索引向量。如果索引在内联后不是编译时常量,它将编译为 store/reload。
#ifdef __GNUC__ // everything except MSVC
float get128_gnuc(__m128 a, int idx) {
return a[idx];
// clang turns it into idx&3
// gcc compiles it exactly like get_reload
}
#endif
# gcc8.1 -O3 -march=haswell
movsx rdi, edi # sign-extend int to pointer width
vmovaps XMMWORD PTR [rsp-24], xmm0 # store into the red-zone
vmovss xmm0, DWORD PTR [rsp-24+rdi*4] # reload
完全可移植的写入方式(256位版本)是:
float get256_reload(__m256 a, int idx) {
// with lower alignment and storeu, compilers still choose to align by 32 because they see the store
alignas(32) float tmp[8];
_mm256_store_ps(tmp, a);
return tmp[idx];
}
编译器需要多条指令来对齐函数的独立版本中的堆栈,但当然在内联之后,这只会发生在外部包含函数中,希望在任何小循环之外。
您可以考虑将 high/low 向量的一半与 vextractf128
和 128 位 vmovups
分开存储,就像 GCC 在不知道时对 _mm256_storeu_ps
所做的那样目的地已对齐,因为 tune=generic(有助于 Sandybridge 和 AMD)。这将避免需要 32 字节对齐的数组,并且对 AMD CPU 基本上没有不利影响。但在 Intel 上与对齐存储相比更糟,因为它需要额外的微指令,假设对齐堆栈的成本可以分摊到许多 get() 操作上。 (使用 __m256
的函数有时最终会对齐堆栈,因此您可能已经付出了代价。)除非您只针对 Bulldozer、Ryzen 和 Sandybridge 等进行调整,否则您可能应该只使用对齐数组。
脚注 1:_mm_extract_ps
returns FP 位模式作为 int
。底层 asm 指令 (extractps r/m32, xmm, imm8
) 可用于将浮点数存储到内存,但不能将元素混洗到 XMM 寄存器的底部。这是 pextrd r/m32, xmm, imm8
.
的 FP 版本
因此您的函数实际上是将整数位模式转换为 FP,使用编译器生成的 cvtsi2ss
,因为 C 允许从 int
隐式转换为 float
。
我想从 simd 寄存器访问任意浮点数。我知道我可以做这样的事情:
float get(const __m128i& a, const int idx){
// editor's note: this type-puns the FP bit-pattern to int and converts to float
return _mm_extract_ps(a,idx);
}
或
float get(const __m128i& a, const int idx){
return _mm_cvtss_f32(_mm_shuffle_ps(a,_MM_SHUFFLE(0,0,0,idx));
}
甚至使用轮班而不是随机播放。问题是这些都需要在编译时知道 idx(shuffle、shift 和 extract 都需要 8 位立即数)。
我也可以使用 _mm_store_ps()
然后使用生成的数组来完成它,但这需要内存。有没有比这更快的方法?
编辑:忽略第一个代码片段,我想要那个位置的浮点数,而不是像 _mm_extract_ps
returns.
首先你肯定不想要_mm_extract_ps
,除非你想把FP打成int
1.
但是无论如何,对于运行时变量索引,您可能不想分支到 select 具有正确 imm8 的指令。
source + asm output for gcc/icc/clang/msvc on the Godbolt compiler explorer 此答案中的所有功能。 包括(在底部)一些使用编译时常量 idx 的测试调用程序,这样您就可以看到在实际程序中发生内联 + 常量传播时会发生什么。 And/or 来自同一向量的两个索引(仅 gcc CSE 和从同一存储重新加载两次,其他编译器存储两次)。
Store/reload 使用 gcc/clang/ICC 进行优化(但可变 idx 版本的延迟更高)。 其他方法只能很好地优化带有 clang 的常量输入。 (clang 甚至可以看穿 pshufb
版本并将其变成 vshufps imm8
或 vpermilps imm8
,或者 idx=0 的空操作)。其他编译器做一些愚蠢的事情,比如用 vxorps
将向量归零并将其用作 vpermilps
控件!
128 位向量:如果您有 SSSE3 pshufb
或 AVX
,请使用变量洗牌
使用 AVX1,对于 128 位向量,使用 vpermilps
,您只需 2 个 ALU 微指令即可完成,这是一个使用双字 [=203] 的变量洗牌=] 或元素,不像 pshufb
.
这使您可以执行与 _mm_shuffle_ps
完全相同的随机播放(包括将低位元素复制到高位 3 个元素,这很好),但使用运行时索引而不是立即数。
// you can pass vectors by value. Not that it matters when inlining
static inline
float get128_avx(__m128i a, int idx){
__m128i vidx = _mm_cvtsi32_si128(idx); // vmovd
__m128 shuffled = _mm_permutevar_ps(a, vidx); // vpermilps
return _mm_cvtss_f32(shuffled);
}
gcc 和 clang 像这样为 x86-64 编译它(Godbolt 编译器资源管理器):
vmovd xmm1, edi
vpermilps xmm0, xmm0, xmm1
ret
没有 AVX 但有 SSSE3,您可以为 pshufb
加载或创建掩码。索引 4 个 __m128i
向量的数组是相当常见的,尤其是使用 _mm_movemask_ps
结果作为索引时。但是这里我们只关心低32位的元素,所以我们可以做得更好。
事实上,模式的常规性质意味着我们可以使用两个 32 位立即数操作数通过乘法和加法来创建它。
static inline
float get128_ssse3(__m128 a, int idx) {
const uint32_t low4 = 0x03020100, step4=0x04040404;
uint32_t selector = low4 + idx*step4;
__m128i vidx = _mm_cvtsi32_si128(selector);
// alternative: load a 4-byte window into 0..15 from memory. worse latency
// static constexpr uint32_t shuffles[4] = { low4, low4+step4*1, low4+step4*2, low4+step4*3 };
//__m128i vidx = _mm_cvtsi32_si128(shuffles[idx]);
__m128i shuffled = _mm_shuffle_epi8(_mm_castps_si128(a), vidx);
return _mm_cvtss_f32(_mm_castsi128_ps(shuffled));
}
-O3 -march=nehalem
的 gcc 输出(其他编译器也这样做,模块可能被浪费了 movaps
):
get128_ssse3(float __vector(4), int):
imul edi, edi, 67372036 # 0x04040404
add edi, 50462976 # 0x03020100
movd xmm1, edi
pshufb xmm0, xmm1
ret # with the float we want at the bottom of XMM0
因此,如果没有 AVX,store/reload 可以节省指令(和 uops),特别是如果编译器可以避免对索引进行符号扩展或零扩展。
从 idx 到结果的延迟 = imul(3) + add(1) + movd(2) + pshufb(1) 在来自 Core2(Penryn) 和更新版本的 Intel CPU 上。不过,从输入向量到结果的延迟仅为 pshufb
。 (加上 Nehalem 上的旁路延迟延迟。)http://agner.org/optimize/
__m256
256 位向量:使用 AVX2 随机播放,否则可能 store/reload
与 AVX1 不同,AVX2 具有像 vpermps
这样的跨车道变量随机播放。 (AVX1 仅对整个 128 位通道进行立即洗牌。)我们可以使用 vpermps
作为 AVX1 vpermilps
的直接替代,从 256 位向量中获取元素。
vpermps
有两个内部函数(参见 Intel's intrinsics finder)。
_mm256_permutevar8x32_ps(__m256 a, __m256i idx)
:旧名称,操作数与 asm 指令相反。_mm256_permutexvar_ps(__m256i idx, __m256 a)
:新名称,随 AVX512 引入,操作数顺序正确(匹配 asm 操作数顺序,与_mm_shuffle_epi8
或_mm_permutevar_ps
相反). asm instruction-set reference manual entry只列出了这个版本,并且列出了错误的类型(__m256 i
用于控制操作数)。gcc 和 ICC 在仅启用 AVX2 而不是 AVX512 的情况下接受此助记符。但不幸的是,clang 只接受
-mavx512vl
(或-march=skylake-avx512
),所以你不能便携地使用它。因此,只需使用笨拙的 8x32 名称,它在任何地方都适用。
#ifdef __AVX2__
float get256_avx2(__m256 a, int idx) {
__m128i vidx = _mm_cvtsi32_si128(idx); // vmovd
__m256i vidx256 = _mm256_castsi128_si256(vidx); // no instructions
__m256 shuffled = _mm256_permutevar8x32_ps(a, vidx256); // vpermps
return _mm256_cvtss_f32(shuffled);
}
// operand order matches asm for the new name: index first, unlike pshufb and vpermilps
//__m256 shuffled = _mm256_permutexvar_ps(vidx256, a); // vpermps
#endif
_mm256_castsi128_si256
从技术上讲并没有留下上层通道未定义(因此编译器永远不需要花费指令零扩展),但无论如何我们不关心上层通道。
这编译为
vmovd xmm1, edi
vpermps ymm0, ymm1, ymm0
# vzeroupper # these go away when inlining
# ret
所以它在 Intel CPU 上非常棒,从输入向量到结果的延迟只有 3c,吞吐量成本为 2 uops(但两个 uops 都需要端口 5)。
AMD 上的车道交叉洗牌要贵得多。
Store/reload
store/reload实际上很好的情况:
- 不带 AVX2 的 256 位向量,或不带 SSSE3 的 128 位向量。
- 如果您需要同一向量中的 2 个或更多元素(但请注意,如果您实际调用
get128_reload
,gcc 以外的编译器会存储多次。因此,如果您这样做,手动内联向量存储并对其进行多次索引。) 当 ALU 端口压力(尤其是 shuffle 端口)成为问题时,吞吐量比延迟更重要。在 Intel CPU 上,
movd xmm, eax
也在端口 5 上运行,因此它与 shuffle 竞争。但希望你只在内部循环之外使用标量提取,周围有很多代码可以做其他事情。当
idx
通常是编译时常量并且您希望让编译器为您选择随机播放时。
一个糟糕的 idx
可能会使您的程序崩溃,而不仅仅是给您错误的元素。 将索引直接转换为随机播放控件的方法会忽略高位。
注意test_reload2
。
Store/reload 到本地数组对于吞吐量来说完全没问题(可能不是延迟),并且在典型的 CPU 上只有大约 6 个周期的延迟,这要归功于存储-转发。大多数 CPU 的前端吞吐量比矢量 ALU 多,因此如果您在 ALU 吞吐量而不是 store/load 吞吐量上接近瓶颈,那么在混合中加入一些 store/reload 一点也不坏。
宽存储可以转发到窄重新加载,但要遵守一些对齐约束。我认为在主流 Intel CPU 上,矢量的 4 或 8 个元素中的任何一个自然对齐的 dword 重新加载都很好,但您可以查看 Intel 的优化手册。请参阅 the x86 tag wiki 中的性能链接。
在 GNU C 中,您可以像数组一样索引向量。如果索引在内联后不是编译时常量,它将编译为 store/reload。
#ifdef __GNUC__ // everything except MSVC
float get128_gnuc(__m128 a, int idx) {
return a[idx];
// clang turns it into idx&3
// gcc compiles it exactly like get_reload
}
#endif
# gcc8.1 -O3 -march=haswell
movsx rdi, edi # sign-extend int to pointer width
vmovaps XMMWORD PTR [rsp-24], xmm0 # store into the red-zone
vmovss xmm0, DWORD PTR [rsp-24+rdi*4] # reload
完全可移植的写入方式(256位版本)是:
float get256_reload(__m256 a, int idx) {
// with lower alignment and storeu, compilers still choose to align by 32 because they see the store
alignas(32) float tmp[8];
_mm256_store_ps(tmp, a);
return tmp[idx];
}
编译器需要多条指令来对齐函数的独立版本中的堆栈,但当然在内联之后,这只会发生在外部包含函数中,希望在任何小循环之外。
您可以考虑将 high/low 向量的一半与 vextractf128
和 128 位 vmovups
分开存储,就像 GCC 在不知道时对 _mm256_storeu_ps
所做的那样目的地已对齐,因为 tune=generic(有助于 Sandybridge 和 AMD)。这将避免需要 32 字节对齐的数组,并且对 AMD CPU 基本上没有不利影响。但在 Intel 上与对齐存储相比更糟,因为它需要额外的微指令,假设对齐堆栈的成本可以分摊到许多 get() 操作上。 (使用 __m256
的函数有时最终会对齐堆栈,因此您可能已经付出了代价。)除非您只针对 Bulldozer、Ryzen 和 Sandybridge 等进行调整,否则您可能应该只使用对齐数组。
脚注 1:_mm_extract_ps
returns FP 位模式作为 int
。底层 asm 指令 (extractps r/m32, xmm, imm8
) 可用于将浮点数存储到内存,但不能将元素混洗到 XMM 寄存器的底部。这是 pextrd r/m32, xmm, imm8
.
因此您的函数实际上是将整数位模式转换为 FP,使用编译器生成的 cvtsi2ss
,因为 C 允许从 int
隐式转换为 float
。