我想从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));
}
,甚至使用shift代替shuffle。问题在于所有这些都要求在编译时知道idx(随机,移位和提取都需要8位立即数)。
我也可以使用_mm_store_ps()
然后使用结果数组来执行此操作,但这需要占用内存。有没有比这更快的方法了?
编辑:忽略第一个代码段,我希望浮点数在该位置,而不是像_mm_extract_ps
返回那样的整数。
答案 0 :(得分:4)
首先,您绝对不希望_mm_extract_ps
,除非您想将FP输入双打到int
1 。
但是无论如何,对于运行时变量索引,您可能不想分支选择具有正确的imm8的指令。
source + asm output for gcc/icc/clang/msvc on the Godbolt compiler explorer,以获取此答案中的所有功能。 (在底部)包括一些使用编译时常数idx的测试调用程序,以便您可以看到在实际程序中进行内联+常数传播时会发生什么。和/或来自同一向量的两个索引(只有gcc CSE并从同一存储重新加载两次,其他编译器存储两次)。
使用gcc / clang / ICC 进行存储/重载优化(但可变IDX版本的延迟较高)。 其他方法只能很好地优化带有clang的常量输入。 (c甚至可以查看pshufb
版本并将其转换为vshufps imm8
或vpermilps imm8
,或者对于idx = 0来说是空操作)。其他编译器会做一些愚蠢的事情,例如用vxorps
将向量归零并将其用作vpermilps
控件!
pshufb
或AVX,请使用可变混洗 对于AVX1,您只能使用vpermilps
在2个ALU指令中对128位向量进行操作,这是一种使用dword选择器元素的变量改组,与{{1} }。
这使您可以执行与pshufb
完全相同的随机播放(包括将低位元素复制到高位3个元素是可以的),但是使用运行时索引而不是立即数。
_mm_shuffle_ps
gcc和clang在x86-64(Godbolt编译器浏览器)中像这样编译它:
// 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);
}
如果没有AVX但具有SSSE3,则可以为 vmovd xmm1, edi
vpermilps xmm0, xmm0, xmm1
ret
加载或创建掩码。对4个pshufb
向量的数组进行索引是相当普遍的,尤其是使用__m128i
结果作为索引。但是这里我们只关心低32位元素,因此我们可以做得更好。
实际上,模式的常规性质意味着我们可以使用两个32位立即数对它们进行乘法和加法运算。
_mm_movemask_ps
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
因此,如果没有AVX,则存储/重载将保存指令(和uops),尤其是在编译器可以避免对索引进行符号扩展或零扩展的情况下。
在Core2(Penryn)及更高版本的Intel CPU上,从idx到结果的延迟= imul(3)+ add(1)+ movd(2)+ pshufb(1)。但是,从输入向量到结果的延迟仅为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
。 (加上Nehalem的旁路延迟延迟。)http://agner.org/optimize/
pshufb
256位向量:使用AVX2随机播放,否则可能会存储/重新加载与AVX1不同,AVX2具有像vpermps
这样的跨行可变混洗。 (AVX1仅立即对整个128位通道进行混洗。)我们可以使用__m256
作为AVX1 vpermps
的直接替代品,以从256位向量中获取元素。
vpermilps
有两个内在函数(请参见Intel's intrinsics finder)。
vpermps
:旧名称,操作数与asm指令的顺序相反。 _mm256_permutevar8x32_ps(__m256 a, __m256i idx)
:AVX512引入的新名称,其操作数具有正确的顺序(匹配_mm256_permutexvar_ps(__m256i idx, __m256 a)
或_mm_shuffle_epi8
的asm操作数顺序)。 asm instruction-set reference manual entry仅列出该版本,并以错误的类型(对于控制操作数为_mm_permutevar_ps
)列出它。
gcc和ICC仅启用AVX2而不接受AVX512接受此助记符。但是不幸的是,clang仅接受__m256 i
(或-mavx512vl
)接受,因此您不能随便使用它。因此,只需使用笨拙的8x32名称即可在任何地方使用。
-march=skylake-avx512
#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
因此,在Intel CPU上,这太棒了,从输入向量到结果只有3c的延迟,并且吞吐成本只有2 upu(但是两个uu都需要端口5)。
AMD上的跨通道改组要贵得多。
实际上存储/重新加载良好的情况:
vmovd xmm1, edi
vpermps ymm0, ymm1, ymm0
# vzeroupper # these go away when inlining
# ret
,则gcc以外的编译器会存储多次。因此,如果这样做,请手动内联向量存储并对其进行多次索引。)当ALU端口压力(尤其是shuffle端口)出现问题时,吞吐量比延迟更重要。在Intel CPU上,get128_reload
也运行在端口5上,因此它与shuffle竞争。但是希望您只在内部循环之外使用标量提取,并且周围的代码可以执行其他操作。
movd xmm, eax
通常是编译时常量,而您想让编译器为您选择随机播放。
糟糕的idx
可能会使程序崩溃,而不仅仅是给您错误的元素。直接将索引转换为随机播放控件的方法会忽略高位。 >
当心ICC sometimes misses optimizing a constant index into a shuffle after inlining。在Godbolt示例中,ICC同意idx
。
存储/重新加载到本地阵列对于吞吐量(可能不是等待时间)完全没问题,并且由于存储转发,在典型的CPU上只有大约6个周期的等待时间。大多数CPU的前端吞吐量都比矢量ALU高,因此,如果您在ALU吞吐量而不是存储/负载吞吐量上遇到瓶颈,那么在混合中包含一些存储/重载一点也不差。
一个广泛的存储可以转发到一个狭窄的重新加载,但要遵守一些对齐约束。我认为向量的4个或8个元素中的任何一个的自然对齐的dword重载在主流Intel CPU上都可以,但是您可以查看Intel的优化手册。请参见the x86 tag wiki中的效果链接。
在GNU C中,您可以像数组一样索引向量。如果索引不是内联后的编译时常量,则将编译为存储/重新加载。
test_reload2
完全可移植的书写方式(256位版本)为:
#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
编译器需要多条指令以使函数的独立版本中的堆栈对齐,但是当然在内联之后,这只会在外部包含函数中发生,希望发生在任何小循环之外。
您可以考虑使用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];
}
和128位vextractf128
分别存储向量的高/低半,就像GCC在vmovups
中不知道目标对齐时一样,用于tune = generic(帮助Sandybridge和AMD)。这样可以避免需要32字节的对齐阵列,而且AMD CPU基本上没有缺点。但是,如果将对齐堆栈的成本分摊到许多get()操作中,则英特尔与统一存储的情况就更糟了,因为它花费了额外的成本。 (使用_mm256_storeu_ps
的函数有时最终还是要对齐堆栈,因此您可能已经付出了代价。)您可能应该只使用对齐数组,除非仅针对Bulldozer,Ryzen和Sandybridge进行调整。
脚注1:__m256
将FP位模式返回为_mm_extract_ps
。底层的asm指令(extractps r/m32, xmm, imm8
)可用于将浮点数存储到内存中,但不能将元素改组到XMM寄存器的底部。这是int
的FP版本。
所以您的函数实际上是使用编译器生成的pextrd r/m32, xmm, imm8
将整数位模式转换为FP,因为C允许从cvtsi2ss
到int
的隐式转换。