如何在不浪费指令清零上层元素的情况下将标量合并到向量中?这是英特尔指令集的设计限制吗?

11

我没有特定的用例; 我想知道这是否真的是英特尔指令集中的设计缺陷/限制,或者我只是忽略了某些东西。

如果要将标量浮点数与现有向量组合,似乎没有办法在不使用高位零扩展或广播标量到向量的情况下使用英特尔指令集。我还没有调查过GNU C本地矢量扩展及其相关内置函数。

如果额外的指令可以被优化掉,那么问题就不会太严重,但是使用gcc(5.4或6.2)时并没有得到优化。也没有好的方法可以使用pmovzxinsertps作为加载,原因是它们的内部函数只接受向量参数。(而且gcc不会将标量->向量加载折叠到汇编指令中。)

__m128 replace_lower_two_elements(__m128 v, float x) {
  __m128 xv = _mm_set_ss(x);        // WANTED: something else for this step, some compilers actually compile this to a separate insn
  return _mm_shuffle_ps(v, xv, 0);  // lower 2 elements are both x, and the garbage is gone
}

使用gcc 5.3 -march=nehalem -O3编译输出,以启用SSE4.1并针对Intel CPU进行调整:(如果没有SSE4.1,则情况会更糟;需要多个指令将上部元素清零)。

    insertps  xmm1, xmm1, 0xe    # pointless zeroing of upper elements.  shufps only reads the low element of xmm1
    shufps    xmm0, xmm1, 0      # The function *should* just compile to this.
    ret

TL:DR:这个问题的其余部分只是在问您是否能够高效地完成此任务,如果不能,为什么不能。
clang的shuffle优化器做得很好,不会浪费指令来将高元素清零(_mm_set_ss(x)),或者将标量复制到它们中(_mm_set1_ps(x))。而不是编写编译器需要优化的内容,难道不能在C中首先“高效”地编写吗?即使是非常近期的gcc也没有优化掉它,所以这是一个真正的(但微不足道的)问题。
如果存在一个标量->128b的等效物__m256 _mm256_castps128_ps256 (__m128 a),那么这将是可能的。即生成一个带有未定义垃圾值的__m128,并且在低元素中保留浮点数,在编译为零汇编指令的情况下,如果标量浮点数/双精度已经在xmm寄存器中,则不需要重新加载。

以下任何内部函数都不存在,但它们应该存在.

  • 一个标量->__m128 等效的 _mm256_castps128_ps256,如上所述。对于标量已经在寄存器中的情况,这是最通用的解决方案。

  • __m128 _mm_move_ss_scalar (__m128 a, float s):用标量s替换向量a的低元素。如果有一个通用的标量->__m128(前面的项目),则实际上不需要这个函数。(movss 的 reg-reg 表单合并,与加载形式不同,也与 movd 不同,后者在两种情况下都将上部元素清零。要复制保存标量浮点数的寄存器而没有错误依赖关系,请使用 movaps)。

  • __m128i _mm_loadzxbd (const uint8_t *four_bytes) 和其他大小的 PMOVZX / PMOVSX:据我所知,没有好的安全方法使用 PMOVZX 内联函数作为加载函数,因为不方便的安全方法不能通过 gcc 进行优化。

  • __m128 _mm_insertload_ps (__m128 a, float *s, const int imm8)INSERTPS 作为加载函数的行为不同:imm8 的上两位被忽略,它总是从有效地址处获取标量(而不是从内存中的向量中获取元素)。这使它能够处理未对齐的 16B 地址,并且即使在未映射页面的 float 右侧也能正常工作。

    与 PMOVZX 一样,gcc 无法将一个清零上部元素的 _mm_load_ss() 折叠到 INSERTPS 的内存操作数中。(请注意,如果 imm8 的上两位都不为零,则 _mm_insert_ps(xmm0, _mm_load_ss(), imm8) 可以编译成 insertps xmm0,xmm0,foo,其中 foo 是不同的 imm8,它将元素在 vec 中清零,就好像 src 元素实际上是由从内存中的 MOVSS 产生的零值。Clang 实际上在这种情况下使用 XORPS/BLENDPS)。


有没有可行的解决方法来模拟其中任何一种,既安全(不会因为例如加载16B而可能触及下一页并导致段错误而在-O0时崩溃),又高效(至少当前gcc和clang没有浪费指令的情况下,在-O3时更好,最好也适用于其他主要编译器)?最好还能以易读的方式呈现,但如果必要,可以将其放在内联包装函数后面,例如__m128 float_to_vec(float a){ something(a); }
英特尔有没有不引入这样的内部函数的充足理由?他们本可以在添加_mm256_castps128_ps256的同时添加一个具有未定义上元素的float->__m128。 这是编译器内部使实现变得困难的问题吗?也许具体来说是ICC内部?
主要的x86-64调用约定(SysV或MS__vectorcall)将第一个FP参数放在xmm0中,并将标量FP参数返回到xmm0中,其中上部元素未定义。(有关ABI文档,请参见标签wiki)。这意味着编译器经常在寄存器中具有标量浮点/双精度数,但上部元素未知。在矢量化内部循环中,这种情况很少见,因此我认为避免这些无用的指令主要只会节省一点代码大小。

pmovzx的情况更加严重:这是您可能在内部循环中使用的内容(例如,对于VPERMD洗牌掩码的LUT,可以将每个索引填充到32位存储器中,从而节省4倍的缓存占用空间)。


pmovzx作为加载的问题一直困扰着我,这个问题的原始版本让我思考了与在xmm寄存器中使用标量浮点数相关的问题。使用pmovzx作为加载的用例可能比标量->__m128更多。


1
我在MSVC上曾经遇到过与此密切相关的问题,次数无法计数。每当涉及_mm_load_*_mm_set_*内联函数时,它都会生成一些真正荒谬的代码。就像这个问题中给出的示例一样,你至少会得到四条指令:movaps xmm2, xmm1; xorps xmm3, xmm3; movss xmm3, xmm2; shufps xmm0, xmm3, 0。我基本上已经放弃了。只要能生成不溢出到内存的汇编代码,我就算是胜利了。 - Cody Gray
1个回答

6

使用GNU C内联汇编是可行的,但这样做很丑陋并破坏了许多优化,包括常量传播(https://gcc.gnu.org/wiki/DontUseInlineAsm)。这不会成为被接受的答案。我将其作为答案添加而非问题的一部分,以便问题保持简短不会变得庞大。

// don't use this: defeating optimizations is probably worse than an extra instruction
#ifdef __GNUC__
__m128 float_to_vec_inlineasm(float x) {
  __m128 retval;
  asm ("" : "=x"(retval) : "0"(x));   // matching constraint: provide x in the same xmm reg as retval
  return retval;
}
#endif

这确实编译成了单个的ret,并且可以内联以让您将标量shufps到向量中:
gcc5.3
float_to_vec_and_shuffle_asm(float __vector(4), float):
    shufps  xmm0, xmm1, 0       # tmp93, xv,
    ret

Godbolt编译器资源管理器上查看此代码

在纯汇编语言中,这显然是微不足道的,你不需要与编译器斗争,以使其不发出你不想要或不需要的指令。


我还没有找到真正的方法来编写一个能够编译成只有一个 ret 指令的 __m128 float_to_vec(float a){ something(a); }。 尝试使用 _mm_undefined_pd()_mm_move_sd() 来处理 double 实际上会使得 gcc 生成更糟糕的代码(请参见上面的 Godbolt 链接)。 现有的float->__m128内部函数 帮不了忙。

离题:_mm_set_ss() 实际代码生成策略:当您编写必须将上部元素清零的代码时,编译器会从一系列有趣的策略中选择。有些很好,有些很奇怪。这些策略在同一编译器(gcc 或 clang)中对于双精度和单精度也有所不同,如您可以在上面的 Godbolt 链接中看到的那样。

一个例子:__m128 float_to_vec(float x){ return _mm_set_ss(x); } 编译为:

    # gcc5.3 -march=core2
    movd    eax, xmm0      # movd xmm0,xmm0 would work; IDK why gcc doesn't do that
    movd    xmm0, eax
    ret

    # gcc5.3 -march=nehalem
    insertps        xmm0, xmm0, 0xe
    ret

    # clang3.8 -march=nehalem
    xorps   xmm1, xmm1
    blendps xmm0, xmm1, 14          # xmm0 = xmm0[0],xmm1[1,2,3]
    ret

给定 __m128 r;,在 MSVC 中使用 r.f32[0] = x;,在 clang 中使用 r[0] = x;,可以达到与 asm ("" : "=x"(retval) : "0"(x)); 相同的效果,而不会失去常量折叠。 - plasmacel
@plasmacel:好主意,但gcc编译它与_mm_set_ss相同,而ICC则完全混乱。https://godbolt.org/g/RC6CWb。gcc确实喜欢使用PXOR或整数XOR来打破虚假依赖关系;我想知道这是否与它非常热衷于将用于未初始化变量的寄存器清零以及清零*相同*寄存器的其余部分有关。 - Peter Cordes
是的,对于GCC和ICC,它会回退到汇编版本。顺便说一下,clang 3.9甚至无法编译汇编版本,可能与编译器bug有关。 - plasmacel
1
使用clang 3.6或更高版本,似乎可以实现以下功能:__m128 retval; memcpy(&retval, &x, sizeof x); return retval;(gcc会生成insertps,对于某些clang版本,通过初始化retval = _mm_undefined_ps();来生成零指令). 通常,clang 4.0或更高版本能够优化您的godbolt链接中许多"干净"变体中的开销. - chtz

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接