我想从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(随机,移位和提取都需要8bit立即数)。
我也可以先使用_mm_store_ps()
然后再使用结果数组来完成此操作,但这需要占用内存。有没有比这更快的方法了?
编辑:忽略第一个代码段,我希望浮点数在该位置,而不是像_mm_extract_ps
返回一样的int 值。
1> Peter Cordes..:
首先,您绝对不想要_mm_extract_ps
,除非您想将FP输入双关键为int
1。
但是无论如何,对于运行时变量索引,您可能不想分支选择具有正确的imm8的指令。
Godbolt编译器资源管理器上gcc / icc / clang / msvc的source + asm输出,用于此答案中的所有功能。 包括(在底部)一些使用编译时常量idx的测试调用程序,以便您可以看到在实际程序中进行内联+常量传播时会发生什么。和/或来自同一向量的两个索引(仅gcc CSE并从同一存储重新加载两次,其他编译器存储两次)。
存储/重新加载使用gcc / clang / ICC进行了优化(但可变idx版本的延迟较高)。 其他方法仅对带有clang的恒定输入进行了优化。(c甚至可以查看该pshufb
版本并将其转换为vshufps imm8
或vpermilps imm8
,或者将idx = 0 设为无操作)。其他编译器也会做一些愚蠢的事情,例如将向量归零vxorps
并将其用作vpermilps
控件!
128位向量:如果您具有SSSE3 pshufb
或AVX,请使用可变随机播放
使用AVX1,您可以使用来在128位向量的2个ALU微指令中完成此操作vpermilps
,这是一个使用dword选择器元素的可变混洗,与不同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));
}
gcc的输出-O3 -march=nehalem
(其他编译器也这样做,模块可能浪费了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,则存储/重载将保存指令(和uops),尤其是在编译器可以避免对索引进行符号扩展或零扩展时。
在Core2(Penryn)及更高版本的Intel CPU上,从idx到结果的延迟= imul(3)+ add(1)+ movd(2)+ pshufb(1)。从输入向量到结果的延迟仅为pshufb
。(加上Nehalem上的旁路延迟延迟。) http://agner.org/optimize/
__m256
256位向量:使用AVX2随机播放,否则可能会存储/重新加载
与AVX1不同,AVX2具有像的交叉通道随机播放 vpermps
。(AVX1仅立即对整个128位通道vpermps
进行了改组。)我们可以用作AVX1的vpermilps
直接替代品,以从256位向量中获取元素。
有两个内部函数vpermps
(请参阅Intel的内部函数查找器)。
_mm256_permutevar8x32_ps(__m256 a, __m256i idx)
:旧名称,操作数与asm指令相反。
_mm256_permutexvar_ps(__m256i idx, __m256 a)
:新名称,随AVX512一起引入,操作数的顺序正确(匹配asm操作数的顺序,与_mm_shuffle_epi8
或相反_mm_permutevar_ps
)。该汇编指令集参考手册条目只列出了这个版本,并列出其与错误的类型(__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 ups(但是两个upu都需要端口5)。
AMD上的过马路改组要贵得多。
存储/重新加载
存储/重新加载实际上很好的情况:
没有AVX2的256位向量,或者没有SSSE3的128位向量。
如果您需要同一向量中的2个或多个元素(但请注意,如果您实际调用gcc以外的编译器,则它们会多次存储get128_reload
。因此,如果这样做,请手动内联向量存储并对其进行多次索引。)
当ALU端口压力(特别是随机端口)出现问题时,吞吐量比延迟更重要。在Intel CPU上,它movd xmm, eax
也运行在端口5上,因此它与shuffle竞争。但是希望您只在内部循环之外使用标量提取,并且周围有很多代码可以完成其他工作。
when idx
通常是编译时常量,您希望让编译器为您选择随机播放。
但是,坏消息idx
可能会使您的程序崩溃,而不仅仅是给您错误的元素。 将索引直接转换为随机播放控件的方法将忽略高位。
注意,内联后,ICC有时会错过将常量索引优化到随机播放中。test_reload2
在Godbolt示例中,ICC可以接受。
存储/重新加载到本地阵列对于吞吐量(可能不是延迟)来说是完全可以的,并且由于存储转发,在典型的CPU上只有大约6个周期的延迟。大多数CPU的前端吞吐量都比矢量ALU高,因此,如果您接近ALU吞吐量而不是存储/负载吞吐量的瓶颈,那么在混合中包含一些存储/重新加载一点也不差。
宽存储可以转发到窄重载,但要遵守一些对齐约束。我认为向量的4个或8个元素中的任何一个的自然对齐的dword重载在主流Intel CPU上都可以,但是您可以查看Intel的优化手册。请参阅x86标签Wiki中的性能链接。
在GNU C中,您可以像数组一样索引向量。如果索引不是内联后的编译时常量,则它将编译为存储/重新加载。
#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];
}
编译器需要多条指令才能使函数的独立版本中的堆栈对齐,但是当然,在内联之后,这仅会在外部包含函数中发生,希望发生在任何小循环之外。
您可以考虑将向量的上半部分/下半部分分别与vextractf128
和128位存储vmovups
,就像GCC _mm256_storeu_ps
在不知道目标位置对齐时所做的那样,适用于tune = generic(帮助Sandybridge和AMD)。这样可以避免需要32字节的对齐阵列,而且AMD CPU基本上没有缺点。但是,如果将对齐堆栈的成本分摊到许多get()操作中,则英特尔与统一存储的情况就更糟了,因为它花费了额外的成本。(__m256
有时使用函数会最终使堆栈对齐,因此您可能已经为此付出了代价。)除非仅针对Bulldozer,Ryzen和Sandybridge等进行调整,否则可能应该只使用对齐数组。
脚注1:_mm_extract_ps
将FP位模式返回为int
。底层的asm指令(extractps r/m32, xmm, imm8
)可用于将浮点数存储到内存中,但不能将元素随机排列到XMM寄存器的底部。这是的FP版本pextrd r/m32, xmm, imm8
。
因此,您的函数实际上是使用编译器生成的cvtsi2ss
,将整数位模式强制转换为FP ,因为C允许从int
to 隐式强制转换float
。