在运行时从simd寄存器获取任意浮点数?

时间:2018-07-18 17:35:12

标签: x86 sse simd avx avx2

我想从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返回那样的整数。

1 个答案:

答案 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 imm8vpermilps imm8,或者对于idx = 0来说是空操作)。其他编译器会做一些愚蠢的事情,例如用vxorps将向量归零并将其用作vpermilps控件!


128位向量:如果您具有SSSE3 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上的跨通道改组要贵得多。


存储/重新加载

实际上存储/重新加载良好的情况:

  • 没有AVX2的256位向量,或者没有SSSE3的128位向量。
  • 如果您需要同一向量中的2个或更多元素(但请注意,如果您实际调用 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允许从cvtsi2ssint的隐式转换。