逻辑SSE内置函数有什么区别?

20

不同类型的逻辑SSE指令是否有区别?例如,如果我们考虑OR操作,有三种指令:_mm_or_ps_mm_or_pd_mm_or_si128,它们都做同样的事情:计算它们的操作数的按位OR。我的问题如下:

  1. 使用其中一种指令(带适当的类型转换)与另一种指令是否有区别?在某些特定情况下是否会存在任何隐藏成本,例如执行时间变长等?

  2. 这些指令映射到三个不同的x86指令(pororpsorpd)。有没有人知道为什么英特尔要浪费宝贵的操作码空间来实现几个做同样事情的指令?


1
请提供需要翻译的英文内容。 - Crashworks
3个回答

20
  1. 使用不同的内置函数(带有适当的类型转换)之间是否有差异?在某些特定情况下,不会存在隐藏成本吗?

是的,在选择一个与另一个之间可能存在性能原因。

1:有时如果整数执行单元的输出需要路由到FP执行单元的输入,或者反之,则会有额外的延迟周期。将128b数据移动到任何可能的目标需要大量电线,因此CPU设计人员必须做出权衡,例如仅从每个FP输出到每个FP输入具有直接路径,而不是所有可能的输入。

请参见this answerAgner Fog的微架构文档以获取旁路延迟信息。在Agner的文档中搜索“ Nehalem上的数据旁路延迟”,其中包含一些很好的实际示例和讨论。他对他分析的每个微体系结构都有一个相关部分。

然而,在Sandy Bridge和Ivy Bridge上,不同域或不同类型寄存器之间传递数据的延迟比Nehalem更小,通常为零。-- Agner Fog的微架构文档
请记住,如果它不在代码的关键路径上(除了Haswell / Skylake有时会在实际旁路之后感染产生的值的后续使用中受到影响),那么延迟并不重要。如果uop吞吐量是瓶颈,而不是关键路径的延迟,则使用pshufd而不是movaps + shufps可能是一种胜利。
2:与其他两个版本相比,...ps版本的遗留SSE编码少1个字节。 (不是AVX)。这将以不同的方式对齐以下指令,这可能对解码器和/或uop缓存行很重要。通常情况下,较小的代码密度更好,可提高I-cache中的代码密度,并将其打包到uop缓存中。 3: 最近的英特尔CPU只能在端口5上运行FP版本。
  • Merom (Core2)和Penryn: orps可以在p0/p1/p5上运行,但仅限于整数域。假定所有3个版本都解码为完全相同的uop。因此发生跨域转发延迟。(AMD CPU也会这样做:FP位运算指令在ivec域中运行。)

  • Nehalem / Sandybridge / IvB / Haswell / Broadwell: por可以在p0/p1/p5上运行,但orps只能在port5上运行。 p5还需要洗牌,但FMA、FP加和FP乘单元在端口0/1上。

  • Skylake: pororps每个周期都有3个吞吐量。英特尔的优化手册提供了一些关于旁路转发延迟的信息:对于/从FP指令,它取决于uop运行的端口。 (通常仍然是端口5,因为FP add/mul/fma单元在端口0和1上。) 另请参见Haswell AVX/FMA latencies tested 1 cycle slower than Intel's guide says - "bypass"延迟可能会影响寄存器的每次使用,直到被覆盖。

请注意,在SnB/IvB(支持AVX但不支持AVX2)上,只有p5需要处理256位逻辑操作,因为vpor ymm, ymm需要AVX2。尽管Nehalem也能做到这一点,但这可能不是更改的原因。
明智的选择方法:
请记住,编译器可以使用por来代替_mm_or_pd,因此其中一些内容仅适用于手写汇编代码。但是,某些编译器会相对忠实地使用您选择的内部函数。
如果端口5上的逻辑操作吞吐量可能成为瓶颈,则即使在FP数据上也要使用整数版本。如果您想使用整数洗牌或其他数据移动指令,则尤其如此。
AMD CPU始终使用整数域进行逻辑运算,因此如果您有多个整数域任务,请一次性完成以最小化域之间的往返。即使依赖链不是代码的瓶颈,较短的延迟也将更快地清除重排序缓冲区中的事物。
如果您只想在FP加法和乘法指令之间设置/清除/翻转FP向量中的位,请使用...ps逻辑,即使在双精度数据上也是如此,因为单精度和双精度FP在现有的每个CPU上都是相同的域,而...ps版本比较短(没有AVX)。
然而,对于使用内部函数的实际/人为因素原因,使用...pd版本更为实用。通过其他人阅读您的代码的可读性是一个因素:他们会想知道为什么您将数据处理为单精度浮点数时实际上是双精度浮点数。对于C/C++内部函数,在__m128__m128d之间进行转换不值得。 (如果没有AVX编译,希望编译器将使用orps来代替_mm_or_pd,这样它实际上可以节省一个字节。)
如果在指令对齐级别上进行调整很重要,请直接使用asm编写,而不是内部函数!(使指令长度增加一个字节可能更好地对齐uop缓存行密度和/或解码器,但是带有前缀和寻址模式 您可以在一般情况下扩展指令
对于整数数据,请使用整数版本。 节省一个指令字节不值得在paddd或其他内容之间绕过延迟,并且整数代码通常会保持端口5完全占用的洗牌。 对于Haswell,许多洗牌/插入/提取/打包/解包指令变为仅p5,而不是SnB / IvB的p1 / p5。 (Ice Lake最终在另一个端口上添加了一个混洗单元,用于一些常见的混洗。)
这些内部函数映射到三个不同的x86指令(pororpsorpd)。 有人有任何想法,为什么英特尔会浪费宝贵的操作码空间来实现相同的功能吗?
如果您查看这些指令集的历史记录,您可以看到我们是如何到达这里的。
por  (MMX):     0F EB /r
orps (SSE):     0F 56 /r
orpd (SSE2): 66 0F 56 /r
por  (SSE2): 66 0F EB /r

MMX在SSE之前存在,因此SSE指令(...ps)的操作码似乎是从相同的0F xx空间中选择的。然后对于SSE2,...pd版本在...ps操作码中添加了一个66操作数大小前缀,整数版本则在MMX版本中添加了一个66前缀。

他们本可以省略orpd和/或por,但他们没有这样做。也许他们认为未来的CPU设计可能会有更长的转发路径连接不同的域,因此使用匹配数据的指令将是更重要的事情。尽管有单独的操作码,但AMD和早期英特尔将它们都视为int-vector。


相关/近似重复:


7

根据英特尔和AMD的优化指南,混合操作类型和数据类型会导致性能下降,因为CPU在内部为特定数据类型标记了64位寄存器的一半。这似乎主要影响流水线,因为指令被解码并安排uops。从功能上讲,它们产生相同的结果。整数数据类型的新版本具有更大的编码,并占用代码段中更多的空间。因此,如果代码大小是一个问题,请使用旧的操作,因为它们具有更小的编码。


混合操作类型和数据类型会导致性能下降...您可以进一步解释一下或给我提供相关参考资料,谢谢。 - user0002128
@user0002128 这是由于数据旁路延迟导致的。 - Noah

3

我认为这三个都是相同的,即128位的按位操作。不同形式存在的原因可能是历史原因,但我不确定。我猜测浮点数版本可能会有一些额外的行为,例如在存在NaN时,但这只是猜测。对于正常输入,这些指令似乎是可互换的,例如:

#include <stdio.h>
#include <emmintrin.h>
#include <pmmintrin.h>
#include <xmmintrin.h>

int main(void)
{
    __m128i a = _mm_set1_epi32(1);
    __m128i b = _mm_set1_epi32(2);
    __m128i c = _mm_or_si128(a, b);

    __m128 x = _mm_set1_ps(1.25f);
    __m128 y = _mm_set1_ps(1.5f);
    __m128 z = _mm_or_ps(x, y);
        
    printf("a = %vld, b = %vld, c = %vld\n", a, b, c);
    printf("x = %vf, y = %vf, z = %vf\n", x, y, z);

    c = (__m128i)_mm_or_ps((__m128)a, (__m128)b);
    z = (__m128)_mm_or_si128((__m128i)x, (__m128i)y);

    printf("a = %vld, b = %vld, c = %vld\n", a, b, c);
    printf("x = %vf, y = %vf, z = %vf\n", x, y, z);
    
    return 0;
}

终端:

$ gcc -Wall -msse3 por.c -o por
$ ./por

a = 1 1 1 1, b = 2 2 2 2, c = 3 3 3 3
x = 1.250000 1.250000 1.250000 1.250000, y = 1.500000 1.500000 1.500000 1.500000, z = 1.750000 1.750000 1.750000 1.750000
a = 1 1 1 1, b = 2 2 2 2, c = 3 3 3 3
x = 1.250000 1.250000 1.250000 1.250000, y = 1.500000 1.500000 1.500000 1.500000, z = 1.750000 1.750000 1.750000 1.750000

ORPD/ORPS只支持SSE,不支持MMX。 - Potatoswatter
1
但是Intel在por之后推出了orps和稍后的orpd。而SSE的物理基础从未发生过太大的变化。 - Potatoswatter
SSE的物理基础已经发生了很大的变化,特别是自从Woodcrest以来,它终于成为一个完整的128位单元。然而这可能是无关紧要的 - 听起来我可能对为什么有单独的按位OR指令的原因感到困惑 - 我认为这是一个遗留问题,与在旧时代在整数和浮点SSE操作之间切换上下文有关,但也许不是。 - Paul R
4
关于第一段的猜测:所有位运算逻辑操作的版本除了指令大小和性能外完全相同。使用位运算FP操作创建NaN不会有任何特殊效果。我不知道是性能(在FP域与向量整数域之间的数据转发)还是程序员友好性/指令集正交性(不需要在FP数据上使用int操作)是更大的激励因素。因为我已经阅读了一些没有人提到的东西,所以我应该写一篇答案... - Peter Cordes
随机交换它们是最好的,通常要避免由于数据旁路延迟,实际上哪些指令会多花费一个周期,这取决于指令/微体系结构,例如在 Nehalem 上shufps / shufd有 1c 旁路延迟,但在 Haswell 上没有。但是,作为一般规则,如果存在与周围相同数据类型的等效执行指令,请使用该指令。 - Noah

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