【问题标题】:What's the difference between logical SSE intrinsics?逻辑 SSE 内在函数之间有什么区别?
【发布时间】:2022-01-18 19:25:51
【问题描述】:

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

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

  2. 这些内在函数映射到三个不同的 x86 指令(pororpsorpd)。有没有人知道为什么英特尔会浪费宝贵的操作码空间来执行相同操作的几条指令?

【问题讨论】:

  • (早先的答案因错误而被删除——我太习惯于 VMX 是我的错)

标签: c sse simd intrinsics sse2


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

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

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

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

但是,在 不同的域或不同类型的寄存器在 Sandy Bridge 和 Ivy Bridge 比上 Nehalem,而且经常为零。 -- Agner Fog 的微拱文档

请记住,如果延迟不在代码的关键路径上(except sometimes on Haswell/Skylake where it infects later use of the produced value,实际绕过后很久:/),延迟并不重要。如果 uop 吞吐量是您的瓶颈,而不是关键路径的延迟,那么使用 pshufd 而不是 movaps + shufps 可能是一个胜利。

2:...ps 版本比其他两个用于传统 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/p5 上运行,但 orps 只能在 port5 上运行。 shuffle 也需要 p5,但 FMA、FP add 和 FP mul 单元位于端口 0/1。

  • Skylake:pororps both have 3-per-cycle throughput。英特尔的优化手册有一些关于绕过转发延迟的信息:往返 FP 指令取决于 uop 在哪个端口上运行。 (通常仍然是端口 5,因为 FP add/mul/fma 单元位于端口 0 和 1。)另请参阅 Haswell AVX/FMA latencies tested 1 cycle slower than Intel's guide says -“绕过”延迟会影响寄存器的每次使用,直到它被覆盖。

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

如何明智地选择

请记住,编译器可以根据需要将por 用于_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 缓存线密度和/或解码器,但具有前缀和寻址模式you can extend instructions in general

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

  1. 这些内在函数映射到三个不同的 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 和早期的 Intel 都将它们视为 int-vector。


相关/接近重复:

【讨论】:

    【解决方案2】:

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

    【讨论】:

    • “将操作类型与数据类型混合会导致性能下降...”您能否进一步解释一下,或者给我相关的参考资料,谢谢。
    • @user0002128 由于Data Bypass Delay
    【解决方案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。
    • 但英特尔推出了orps 和后来的orpd 两者都在 por之后。 SSE 的物理基础从未发生太大变化。
    • SSE 的物理基础已经改变了很多,特别是自从 Woodcrest 最终成为一个完整的 128 位单元之后。然而,这可能无关紧要 - 听起来我可能错了为什么会有单独的按位 OR 指令 - 我认为这是过去在整数和浮点 SSE 操作之间切换上下文的遗留问题,但也许不是。
    • re:第一段中的推测:除了指令大小和性能之外,所有版本的按位逻辑操作都完全相同。使用按位 FP 操作创建 NaN 不会做任何特别的事情。 IDK 如果性能(使用 FP 域与 vector-int 域进行数据转发)或程序员友好性/insn 设置正交性(不必对 FP 数据使用 int ops)是更大的激励因素。我应该写一个答案,因为我读过一些没人提到的东西......
    • 最好避免随机交换它们,因为Data Bypass Delay 哪些指令实际上需要额外的周期是非常依赖于指令/微拱的,即在 Nehalem 上有一个 1c 的旁路延迟 shufps / @ 987654328@ 但在 haswell 上没有。但作为一般规则,如果存在与周围数据类型相同的数据类型相同性能的指令。
    猜你喜欢
    • 1970-01-01
    • 2019-06-08
    • 1970-01-01
    • 2013-09-08
    • 2011-09-01
    • 2020-09-07
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多