如何在没有编译器浪费指令归零上层元素的情况下将标量合并到向量中?英特尔内在函数的设计限制?

时间:2016-09-04 15:24:05

标签: c gcc x86 sse intrinsics

我没有考虑特定的用例;我问这是否真的是英特尔内在函数中的设计缺陷/限制,或者我是否遗漏了某些东西。

如果你想将标量浮点数与现有向量相结合,那么使用英特尔内在函数似乎没有高元素归零或将标量广播到向量中的方法。我还没有研究过GNU C本机向量扩展和相关的内置函数。

如果额外的内在优化消失,这不会太糟糕,但它不适用于gcc(5.4或6.2)。使用pmovzxinsertps作为载荷也没有好办法,因为他们的内在函数只采用向量args的相关原因。 (并且gcc不会将标量 - >向量加载到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并调整该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-optimizer得到了这个权利,并没有浪费指令归零高元素(_mm_set_ss(x)),或者将标量复制到它们中(_mm_set1_ps(x))。编写器必须优化而不是编写某些东西,而不应该有一种方法来编写它,并且#34;有效地"在C中首先?即使是非常近期的gcc 也没有优化它,所以这是一个真实的(但很小的)问题。

如果有一个标量 - > 128b等效的__m256 _mm256_castps128_ps256 (__m128 a),这是可能的。即,如果标量float / double已经在xmm寄存器中,则在上部元素中生成带有未定义垃圾的__m128,并在低元素中生成浮点数,编译为零asm指令。

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

  • 如上所述的标量 - > __ m128当量_mm256_castps128_ps256。标量已经存在的最常见的解决方案。
  • __m128 _mm_move_ss_scalar (__m128 a, float s):用标量a替换向量s的低元素。如果有通用标量 - > __ m128(前一个要点),这实际上并不是必需的。 (movss的reg-reg形式合并,与零形式的加载形式不同,并且与movd不同,它们在两种情况下都将上层元素归零。要复制一个包含标量浮点而没有错误依赖的寄存器,请使用{ {3}})。
  • __m128i _mm_loadzxbd (const uint8_t *four_bytes)以及其他尺寸的movaps / PMOVSX:PMOVZX,因为不方便的安全方式并没有使用gcc进行优化。
  • __m128 _mm_insertload_ps (__m128 a, float *s, const int imm8)AFAICT, there's no good safe way to use the PMOVZX intrinsics as a load作为一个加载行为不同:imm8的高2位被忽略,它总是将标量置于有效地址(而不是内存中向量的元素)。这使得它可以处理不是16B对齐的地址,如果float在未映射的页面之前,则可以正常工作。

    与PMOVZX一样,gcc无法将上元素归零_mm_load_ss()折叠到INSERTPS的内存操作数中。 (注意,如果imm8的高2位都不为零,那么_mm_insert_ps(xmm0, _mm_load_ss(), imm8)可以编译为insertps xmm0,xmm0,foo,使用不同的imm8,如果src元素是,则将vec中的元素归为零。实际上是MOVSS从内存中产生的零。在这种情况下,Clang实际上使用了XORPS / BLENDPS)

是否有任何可行的解决方法来模拟任何安全的(不要在-O0中断,例如加载可能触及下一页和段错误的16B),以及高效(至少在当前的gcc和clang上没有浪费指令-O3,最好是其他主要的编译器)?最好也是以可读的方式,但如果有必要,它可以放在内联包装函数之后,如__m128 float_to_vec(float a){ something(a); }

英特尔没有任何理由不引入这样的内在函数吗?他们可以在添加_mm256_castps128_ps256的同时添加一个浮点数> __ m128,其中包含未定义的上层元素。 这是编译器内部的问题,难以实现吗?或许特别是ICC内部?

x86-64(SysV或MS __vectorcall)上的主要调用约定采用xmm0中的第一个FP arg,并返回xmm0中的标量FP args,其中上部元素未定义。 (有关ABI文档,请参阅INSERTPS标记wiki)。这意味着编译器在具有未知上层元素的寄存器中具有标量float / double并不罕见。这在矢量化内循环中很少见,因此我认为避免这些无用的指令通常只会节省一些代码大小。

pmovzx情况更严重:这可能是你在内部循环中使用的东西(例如,对于VPERMD shuffle掩码的LUT,在缓存占用中保存4倍而在内存中存储填充为32位的每个索引)

pmovzx-as-a-load问题一直困扰着我一段时间,让我想到了在xmm寄存器中使用标量浮点数的相关问题。作为负载,pmovzx可能有更多用例,而不是标量 - > __ m128。

1 个答案:

答案 0 :(得分:4)

它可以使用GNU C inline asm,但这很丑陋并且无法实现许多优化,包括常量传播(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 compiler explorer 上查看此代码。

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

我还没有找到任何真正的方法来编写只编译__m128 float_to_vec(float a){ something(a); }指令的retdouble使用_mm_undefined_pd()_mm_move_sd()的尝试实际上会使用gcc编写更糟糕的代码(请参阅上面的Godbolt链接)。没有the existing float->__m128 intrinsics帮助。

偏离主题:实际的_mm_set_ss()代码生成策略:当您编写的代码必须为零元素时,编译器会从一系列有趣的策略中选择。有些好,有些奇怪。在同一个编译器(gcc或clang)上,double和float之间的策略也不同,正如你在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