c - 如何将标量合并为 vector 而不编译器浪费指令将上元素归零?英特尔内在函数的设计限制?

标签 c gcc x86 sse intrinsics

我没有想到特定的用例;我在问这是否真的是英特尔内在函数的设计缺陷/限制,或者我是否只是遗漏了一些东西。
如果您想将标量浮点数与现有 vector 相结合,似乎没有办法在没有高元素归零或使用 Intel 内在函数将标量广播到 vector 的情况下做到这一点。我还没有研究过 GNU C 原生 vector 扩展和相关的内置函数。
如果额外的内在优化消失,这不会太糟糕,但它不会与 gcc(5.4 或 6.2)。也没有很好的使用方法 pmovzxinsertps作为负载,由于相关原因,它们的内在函数仅采用 vector 参数。 (并且 gcc 不会将标量-> vector 加载折叠到 asm 指令中。)

__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 并针对该英特尔 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-optimizer 正确地做到了这一点,并且不会浪费在将高元素归零( _mm_set_ss(x) )或将标量复制到它们( _mm_set1_ps(x) )上的指令。与其编写编译器必须优化的东西,不如首先用 C 语言“有效地”编写它?即使是最近的 gcc 也没有优化它,所以这是一个真正的(但次要的)问题。

如果有一个标量-> 128b 等效于 __m256 _mm256_castps128_ps256 (__m128 a) ,这将是可能的。 .即产生一个 __m128上元素中有未定义的垃圾,下元素中有浮点数,如果标量浮点数/ double 数已经在 xmm 寄存器中,则编译为零 asm 指令。
以下内在函数都不存在,但它们应该 .
  • 标量->__m128 等效于 _mm256_castps128_ps256如上所述。标量已注册情况的最通用解决方案。
  • __m128 _mm_move_ss_scalar (__m128 a, float s) : 替换 vector 的低元素 a带标量 s .如果有通用标量->__m128(上一个要点),这实际上不是必需的。 ( movss 的 reg-reg 形式合并,不像加载形式归零,也不像 movd 在这两种情况下都归零上元素。要复制一个寄存器保存一个没有错误依赖的标量浮点数,使用 movaps )。
  • __m128i _mm_loadzxbd (const uint8_t *four_bytes)和其他尺寸的 PMOVZX/PMOVSX:AFAICT, there's no good safe way to use the PMOVZX intrinsics as a load ,因为不方便的安全方式不会用 gcc 优化掉。
  • __m128 _mm_insertload_ps (__m128 a, float *s, const int imm8) . INSERTPS与加载的行为不同:忽略 imm8 的高 2 位,并且它始终采用有效地址处的标量(而不是内存中 vector 的元素)。这使它可以处理非 16B 对齐的地址,并且即使在 float 出现错误的情况下也能正常工作。就在未映射的页面之前。
    与 PMOVZX 一样,gcc 无法折叠上元素归零 _mm_load_ss()到 INSERTPS 的内存操作数中。 (请注意,如果 imm8 的高 2 位不都为零,则 _mm_insert_ps(xmm0, _mm_load_ss(), imm8) 可以编译为 insertps xmm0,xmm0,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)上的主要调用约定采用 xmm0 中的第一个 FP arg 并返回 xmm0 中的标量 FP args,上层元素未定义。 (有关 ABI 文档,请参阅 标签维基)。这意味着编译器在具有未知上层元素的寄存器中具有标量浮点数/ double 数的情况并不少见。这在矢量化内循环中很少见,所以我认为避免这些无用的指令只会节省一些代码大小。
    pmovzx 情况更严重:这是您可能在内部循环中使用的东西(例如,对于 VPERMD shuffle 掩码的 LUT,与将每个索引填充到内存中的 32 位相比,缓存占用空间节省 4 倍)。

    pmovzx-as-a-load 问题已经困扰我一段时间了,the original version of this question让我思考在 xmm 寄存器中使用标量浮点数的相关问题。 pmovzx 作为负载的用例可能比标量->__m128 的用例更多。

    最佳答案

    它在 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将标量转换为 vector :
    gcc5.3
    float_to_vec_and_shuffle_asm(float __vector(4), float):
        shufps  xmm0, xmm1, 0       # tmp93, xv,
        ret
    

    Godbolt compiler explorer 上查看此代码 .

    这在纯汇编语言中显然是微不足道的,在这种情况下,您不必与编译器进行斗争以使其不发出您不想要或不需要的指令。

    我还没有找到任何真正的方法来写 __m128 float_to_vec(float a){ something(a); }编译成一个 ret操作说明。对 double 的尝试使用 _mm_undefined_pd()_mm_move_sd()实际上使用 gcc 使代码更糟(请参阅上面的 Godbolt 链接)。 the existing float->__m128 intrinsics 都没有帮助。

    题外话:实际 _mm_set_ss() 代码生成策略 :当您编写必须将上层元素归零的代码时,编译器会从一系列有趣的策略中进行选择。有些不错,有些奇怪。如您在上面的 Godbolt 链接中所见,同一编译器(gcc 或 clang)上的 double 和 float 之间的策略也有所不同。

    一个例子:__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
    

    关于c - 如何将标量合并为 vector 而不编译器浪费指令将上元素归零?英特尔内在函数的设计限制?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/39318496/

    相关文章:

    c - 如何使while循环中的随机数相互独立?

    gcc - ld 找不到库,即使 ldconfig 找到了 (rust-sciter)

    c++ - 在C/C++编程中,从较小的地址减去较大的地址时,输出指示什么?

    gcc - 编译 WITH_PIC (-DWITH_PIC, --with-pic) 实际上有什么作用?

    multithreading - CPU 什么时候可以忽略 LOCK 前缀并使用缓存一致性?

    通过指针改变值

    c - C中如何确定单独整数的顺序?

    C:在文件之间传递变量

    performance - 直接比较 ARM 和 x86 处理器的时钟频率有多公平?

    assembly - x86:ZF 并不总是由 AND 更新?