我没有特定的用例; 我想知道这是否真的是英特尔指令集中的设计缺陷/限制,或者我只是忽略了某些东西。
如果要将标量浮点数与现有向量组合,似乎没有办法在不使用高位零扩展或广播标量到向量的情况下使用英特尔指令集。我还没有调查过GNU C本地矢量扩展及其相关内置函数。
如果额外的指令可以被优化掉,那么问题就不会太严重,但是使用gcc(5.4或6.2)时并没有得到优化。也没有好的方法可以使用pmovzx
或insertps
作为加载,原因是它们的内部函数只接受向量参数。(而且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文档,请参见x86标签wiki)。这意味着编译器经常在寄存器中具有标量浮点/双精度数,但上部元素未知。在矢量化内部循环中,这种情况很少见,因此我认为避免这些无用的指令主要只会节省一点代码大小。
pmovzx的情况更加严重:这是您可能在内部循环中使用的内容(例如,对于VPERMD洗牌掩码的LUT,可以将每个索引填充到32位存储器中,从而节省4倍的缓存占用空间)。
pmovzx作为加载的问题一直困扰着我,这个问题的原始版本让我思考了与在xmm寄存器中使用标量浮点数相关的问题。使用pmovzx作为加载的用例可能比标量->__m128更多。
_mm_load_*
或_mm_set_*
内联函数时,它都会生成一些真正荒谬的代码。就像这个问题中给出的示例一样,你至少会得到四条指令:movaps xmm2, xmm1; xorps xmm3, xmm3; movss xmm3, xmm2; shufps xmm0, xmm3, 0
。我基本上已经放弃了。只要能生成不溢出到内存的汇编代码,我就算是胜利了。 - Cody Gray