SSE 和 AVX 中没有 float/double 的插入和提取?

No insert and extract for float/double in SSE and AVX?

我刚刚注意到缺少 _mm256_insert_pd()/_mm256_insert_ps()/_mm_insert_pd()_mm_insert_ps() 也存在,但使用模式有些奇怪。

同时存在 _mm_insert_epi32() and _mm256_insert_epi32() 和其他整数变体。

英特尔是否出于某种原因有意不实施 float/double 变体?在 SSE/AVX 寄存器的给定位置(不仅是第 0 个)设置单个 float/double 的正确且性能最好的方法是什么?

我实现了 insert 的 AVX-double 变体,它有效,但也许还有更好的方法来做到这一点:

Try it online!

template <int I>
__m256d _mm256_insert_pd(__m256d a, double x) {
    int64_t ix;
    std::memcpy(&ix, &x, sizeof(x));
    return _mm256_castsi256_pd(
        _mm256_insert_epi64(_mm256_castpd_si256(a), ix, I)
    );
}

正如我所见,extract float/double 由于某种原因在 SSE/AVX 中也没有变体。我只知道 _mm_extract_ps() 存在,其他的不存在。

你知道为什么 float/double SSE/AVX 没有 insertextract 吗?

标量 float/double 已经是 XMM/YMM 寄存器的底部元素,并且有各种 FP 洗牌指令,包括 vinsertpsvmovlhps 可以(在asm) 插入 32 位或 64 位元素。但是,没有适用于 256 位 YMM 寄存器的版本,并且一般的 2 寄存器洗牌直到 AVX-512 才可用,并且只能使用矢量控制。

仍然有很多困难在于内在函数 API,这使得获得有用的 asm 操作变得更加困难。


一个不错的方法是广播标量 float 或 double 并混合,部分原因是广播是内在函数已经提供的获取包含标量 __m256d 的方法之一 1.

立即混合指令可以有效地替换另一个向量的一个元素,即使在高半部分2。它们在大多数 AVX CPU 上具有良好的吞吐量和延迟以及后端端口分布。它们需要 SSE4.1,但使用 AVX 它们始终可用。

(另请参阅 Agner Fog 的 VectorClass Library (VCL),用于替换矢量元素的 C++ 模板;具有各种 SSE/AVX 功能级别。包括运行时变量索引,但通常旨在优化到好的东西对于编译时常量,例如 Vec4f::insert())

中的索引开关

float 变成 __m256

template <int pos>
__m256 insert_float(__m256 v, float x) {
    __m256 xv = _mm256_set1_ps(x);
    return _mm256_blend_ps(v, xv, 1<<pos);
}

最好的情况是位置=0。 (Godbolt)

auto test2_merge_0(__m256 v, float x){
    return insert_float<0>(v,x);
}

clang 注意到广播是多余的并对其进行了优化:

test2_merge_0(float __vector(8), float):
        vblendps        ymm0, ymm0, ymm1, 1             # ymm0 = ymm1[0],ymm0[1,2,3,4,5,6,7]
        ret

但是 clang 有时会变得过于聪明而不利于自身利益,并将其悲观化为

test2_merge_5(float __vector(8), float):  # clang(trunk) -O3 -march=skylake
        vextractf128    xmm2, ymm0, 1
        vinsertps       xmm1, xmm2, xmm1, 16    # xmm1 = xmm2[0],xmm1[0],xmm2[2,3]
        vinsertf128     ymm0, ymm0, xmm1, 1
        ret

或者合并到归零向量时,clang 使用 vxorps-归零然后混合,但 gcc 做得更好:

test2_zero_0(float):           # GCC(trunk) -O3 -march=skylake
        vinsertps       xmm0, xmm0, xmm0, 0xe
        ret

脚注 1:
这对内在函数来说是个问题;您可以与标量 float/double 一起使用的许多内在函数仅适用于向量操作数,并且编译器并不总是设法优化掉 _mm_set_ss_mm_set1_ps 或任何当您只实际阅读底部元素。标量 float/double 要么在内存中,要么已经在 X/YMM 寄存器的底部元素中,因此在 asm 中,可以 100% 自由地对已经加载到寄存器中的标量浮点数/双精度数使用向量随机播放。

但是没有内在函数告诉编译器你想要一个底部外有无关元素的向量。这意味着您必须以一种看起来像是在做额外工作的方式编写源代码,并依靠编译器对其进行优化。 How to merge a scalar into a vector without the compiler wasting an instruction zeroing upper elements? Design limitation in Intel's intrinsics?

脚注 2:
不像 vpinsrq。正如您从 Godbolt 中看到的那样,您的版本编译效率非常低,尤其是使用 GCC 时。他们必须单独处理 __m256d 的高半部分,尽管 GCC 找到了更少的优化方式并使 asm 更接近您非常低效的代码。顺便说一句,使函数 return 成为 __m256d 而不是分配给 volatile;这样你就有更少的噪音。 https://godbolt.org/z/Wrn7n4soh)

_mm256_insert_epi64 是一个“复合”内在/辅助函数:vpinsrq 仅以 vpinsrq xmm, xmm, r/m64, imm8 形式可用,它将 xmm 寄存器零扩展为完整的 Y/ZMM.即使是 clang 的 shuffle 优化器(发现 vmovlhps 以用另一个 XMM 的低半部分替换 XMM 的高半部分)当您混合到现有向量而不是零时,最终仍然会提取并重新插入高半部分。


asm 情况是 extractps 的标量操作数是 r/m32,而不是 XMM 寄存器,因此它对提取标量浮点数没有用(除了将其存储到内存中)。有关更多信息,请参阅 my answer on the Q&A Intel SSE: Why does `_mm_extract_ps` return `int` instead of `float`?insertps

insertps xmm, xmm/m32, imm 可以 select 来自另一个向量寄存器的源浮点数,所以唯一的内在函数需要两个向量,给你留下 How to merge a scalar into a vector without the compiler wasting an instruction zeroing upper elements? Design limitation in Intel's intrinsics? 当你只关心底部的元素时,说服编译器不要浪费指令设置元素的问题 __m128