20

不同类型的逻辑 SSE 内在函数之间有什么区别吗?例如,如果我们进行 OR 运算,则有三个内在函数:_mm_or_ps,它们_mm_or_pd_mm_or_si128做同样的事情:计算它们的操作数的按位或。我的问题:

  1. 使用一个或另一个内在函数(使用适当的类型转换)有什么区别。在某些特定情况下会不会有任何隐性成本,例如更长的执行时间?

  2. 这些内在函数映射到三个不同的 x86 指令 ( por, orps, orpd)。有谁知道为什么英特尔将宝贵的操作码空间浪费在几条做同样事情的指令上?

4

3 回答 3

19
  1. 使用一个或另一个内在函数(使用适当的类型转换)有什么区别。在某些特定情况下会不会有任何隐性成本,例如更长的执行时间?

是的,选择一个与另一个可能有性能原因。

1:如果整数执行单元的输出需要路由到 FP 执行单元的输入,有时会有一两个额外的延迟周期(转发延迟),反之亦然。将 128b 的数据移动到许多可能的目的地中的任何一个都需要很多线,因此 CPU 设计人员必须做出权衡,例如只有从每个 FP 输出到每个 FP 输入的直接路径,而不是到所有可能的输入。

有关旁路延迟,请参阅此答案Agner Fog 的微架构文档。在 Agner 的文档中搜索“Nehalem 的数据绕过延迟”;它有一些很好的实际例子和讨论。对于他分析的每个微架构,他都有一个部分。

然而,在不同域或不同类型的寄存器之间传递数据的延迟在 Sandy Bridge 和 Ivy Bridge 上比在 Nehalem 上要小,并且通常为零。-- Agner Fog 的微拱文档

请记住,如果延迟不在代码的关键路径上,则延迟无关紧要(有时在 Haswell/Skylake 上,它会在实际绕过后很长时间感染生成值的后续使用:/)。如果 uop 吞吐量是您的瓶颈,而不是关键路径的延迟,则使用pshufd代替可能是一个胜利。movaps + shufps

2:...ps对于 legacy-SSE 编码,该版本比其他两个少 1 个字节的代码。(不是 AVX)。这将以不同的方式对齐以下指令,这对于解码器和/或 uop 缓存线可能很重要。通常越小越好,因为 I-cache 中的代码密度更高,从 RAM 中获取代码并打包到 uop 缓存中。

3:最近的 Intel CPU 只能在 port5 上运行 FP 版本。

  • Merom (Core2) 和 Penryn:orps可以在 p0/p1/p5 上运行,但只能在整数域上运行。大概所有 3 个版本都解码为完全相同的 uop。所以就发生了跨域转发延迟。(AMD CPU 也这样做:FP 位指令在 ivec 域中运行。)

  • Nehalem / Sandybridge / IvB / Haswell / Broadwell:por可以在 p0/p1/p5orps上运行,但只能在 port5 上运行。shuffle 也需要 p5,但 FMA、FP add 和 FP mul 单元位于端口 0/1 上。

  • Skylake:por两者orps 都有 3-per-cycle 吞吐量。英特尔的优化手册有一些关于绕过转发延迟的信息:往返 FP 指令取决于 uop 在哪个端口上运行。(通常仍然是端口 5,因为 FP add/mul/fma 单元位于端口 0 和 1 上。)另见Haswell AVX/FMA 延迟测试比英特尔指南所说的慢 1 个周期- “绕过”延迟会影响寄存器的每次使用,直到它被覆盖了。

请注意,在 SnB/IvB(AVX 但不是 AVX2)上,只有 p5 需要处理 256b 逻辑操作,因为vpor ymm, ymm需要 AVX2。这可能不是改变的原因,因为 Nehalem 是这样做的。

如何明智地选择

请记住,编译器可以根据需要使用porfor _mm_or_pd,因此其中一些主要适用于手写 asm。但是有些编译器在某种程度上忠实于您选择的内在函数。

如果端口 5 上的逻辑运算吞吐量可能成为瓶颈,则使用整数版本,即使是 FP 数据也是如此。如果您想使用整数洗牌或其他数据移动指令,则尤其如此。

AMD CPU 始终使用整数域进行逻辑运算,因此如果您有多个整数域的事情要做,请一次性完成所有任务,以最大限度地减少域之间的往返。即使 dep 链不是您的代码的瓶颈,更短的延迟将更快地从重新排序缓冲区中清除内容。

如果您只想在 FP add 和 mul 指令之间的 FP 向量中设置/清除/翻转一点,请使用...ps逻辑,即使是在双精度数据上,因为单 FP 和双 FP 在现有的每个 CPU 上都是相同的域,并且...ps版本短一个字节(没有 AVX)。

...pd但是,使用带有内在函数的版本有实际/人为因素的原因。其他人对您的代码的可读性是一个因素:他们会想知道为什么您将数据视为单数,而实际上它是双数的。对于 C/C++ 内在函数,在代码中乱扔和之间的转换__m128__m128d不值得的。(并且希望编译器无论如何都会使用orps_mm_or_pd如果在没有 AVX 的情况下编译它实际上会节省一个字节。)

如果调整 insn 对齐的级别很重要,请直接用 asm 编写,而不是内在函数!(将指令延长一个字节可能会更好地为 uop 缓存线密度和/或解码器对齐,但使用前缀和寻址模式,您通常可以扩展指令

对于整数数据,请使用整数版本。保存一个指令字节不值得在两者之间进行旁路延迟paddd或其他任何事情,并且整数代码通常使端口5完全被洗牌占据。对于 Haswell,许多 shuffle / insert / extract / pack / unpack 指令仅变为 p5,而不是 SnB/IvB 的 p1/p5。(Ice Lake 终于在另一个端口上添加了一个 shuffle 单元,用于一些更常见的 shuffle。)

  1. 这些内在函数映射到三个不同的 x86 指令 ( por, orps, orpd)。有谁知道为什么英特尔将宝贵的操作码空间浪费在几条做同样事情的指令上?

如果您查看这些指令集的历史,您可以了解我们是如何到达这里的。

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版本为操作码添加了66操作数大小的前缀...ps,整数版本66为 MMX 版本添加了前缀。

他们本可以省略orpd和/或por,但他们没有。也许他们认为未来的 CPU 设计可能在不同域之间有更长的转发路径,因此为您的数据使用匹配指令将是一件大事。即使有单独的操作码,AMD 和早期的 Intel 都将它们视为 int-vector。


相关/接近重复:

于 2015-07-05T17:22:41.297 回答
7

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

于 2010-08-20T19:36:04.517 回答
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
于 2010-05-10T18:42:10.427 回答