逻辑SSE内在函数之间有什么区别?

时间:2010-05-10 17:32:08

标签: c sse simd intrinsics sse2

不同类型的逻辑SSE内在函数之间是否有任何区别?例如,如果我们采用OR运算,有三个内在函数:_mm_or_ps,_mm_or_pd和_mm_or_si128所有这些都做同样的事情:计算按位或者它们的操作数。我的问题:

  1. 使用一个或另一个内在(使用适当的类型转换)之间是否有任何区别。在某些特定情况下,不会有更长时间执行等隐藏成本吗?

  2. 这些内在函数映射到三个不同的x86指令(por,orps,orpd)。有没有人有任何想法为什么英特尔会浪费宝贵的操作码空间来执行同样的操作呢?

3 个答案:

答案 0 :(得分:12)

  
      
  1. 使用一个或另一个内在(使用适当的类型转换)之间是否有任何区别。不知道在某些特定情况下会有更长时间的执行等隐藏成本吗?
  2.   

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

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

请参阅this answerAgner Fog's microarchitecture doc了解旁路延迟。在Nehalem"上搜索"数据旁路延迟在阿格纳的博士;它有一些很好的实际例子和讨论。对于他分析过的每一个微博,他都有一个部分。

  

然而,在之间传递数据的延迟   不同的域或不同类型的寄存器较小   Sandy Bridge和Ivy Bridge比Nehalem还要零。 -   Agner Fog的微型博士文档

请记住,如果延迟不在代码的关键路径上,那么延迟并不重要。如果uop吞吐量是您的瓶颈,而不是关键路径的延迟,则使用pshufd代替movaps + shufps可能会获胜。

2: ...ps版本的代码字节数少于其他两个字节。这将以不同方式对齐以下指令,这对解码器和/或uop缓存行很重要。

3:最近的英特尔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上运行。 shuff还需要p5,但FMA,FP add和FP mul单元位于端口0/1上。

  • Skylake:pororps both have 3-per-cycle throughput。有关转发延迟的信息尚不可用。

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

如何明智地选择

如果port5上的逻辑运算吞吐量可能成为瓶颈,那么即使在FP数据上也使用整数版本。如果要使用整数shuffle或其他数据移动指令,则尤其如此。

AMD CPU始终使用整数域作为逻辑,因此如果您要执行多个整数域操作,请一次性完成这些操作以最大限度地减少域之间的往返。较短的延迟会更快地从重新排序缓冲区中清除,即使dep链不是代码的瓶颈。

如果您只想在FP add和mul指令之间设置/清除/翻转FP矢量中的某个位,请使用...ps逻辑,即使是双精度数据,因为单FP和双FP是相同的域在每个存在的CPU上,...ps版本缩短一个字节。

使用...pd版本存在实际/人为因素,这通常会超过保存1个字节的代码。其他人对您的代码的可读性是一个因素:他们想知道为什么当您的数据实际翻倍时,他们会将您的数据视为单身。 ESP。使用C / C ++内在函数,在__mm256__mm256d之间使用强制转换来乱丢代码是不值得的。如果调整insn对齐的级别很重要,请直接写入asm,而不是内在函数! (将指令延长一个字节可能会使uop缓存线密度和/或解码器更好地对齐。)

对于整数数据,请使用整数版本。保存一个指令字节不值得旁路延迟,整数代码通常使port5完全被shuffle占用。对于Haswell,许多shuffle / insert / extract / pack / unpack指令仅变为p5,而不是sn1 / IvB的p1 / p5。

  
      
  1. 这些内在函数映射到三个不同的x86指令(pororps,   orpd)。有没有人有任何想法为什么英特尔浪费宝贵的操作码   几个指令的空间做同样的事情?
  2.   

如果你看一下这些指令集的历史,你可以看看我们是如何到达这里的。

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

他们可能遗漏了orpd和/或por,但他们没有。也许他们认为未来的CPU设计可能在不同的域之间有更长的转发路径,因此使用匹配的数据指令将是一个更大的交易。即使有单独的操作码,AMD和早期的英特尔也将它们视为int-vector。

答案 1 :(得分:7)

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

答案 2 :(得分:3)

我认为这三者实际上是相同的,即128位按位运算。不同形式存在的原因可能是历史性的,但我不确定。我想这是可能在浮点版本中可能存在一些额外的行为,例如当有NaNs时,这是纯粹的猜测。对于正常输入,指令似乎是可互换的,例如

#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