X86 使用SSE计算绝对值的最快方法

X86 使用SSE计算绝对值的最快方法,x86,vectorization,sse,simd,absolute-value,X86,Vectorization,Sse,Simd,Absolute Value,我知道有3种方法,但据我所知,通常只使用前2种: 使用和ps或和notps屏蔽符号位 优点:如果掩码已经在寄存器中,则一条快速指令,这使得它非常适合在循环中多次执行此操作 缺点:掩码可能不在寄存器中,或者更糟,甚至不在缓存中,这会导致很长的内存提取 从零减去该值求反,然后得到原始值和求反值的最大值 优点:固定成本,因为不需要任何东西,比如面具 缺点:如果条件理想,将始终比掩码方法慢,并且在使用maxps指令之前,我们必须等待subps完成 与选项2类似,将原始值从零减去求反,然后使用和p

我知道有3种方法,但据我所知,通常只使用前2种:

  • 使用
    和ps
    和notps
    屏蔽符号位

    • 优点:如果掩码已经在寄存器中,则一条快速指令,这使得它非常适合在循环中多次执行此操作
    • 缺点:掩码可能不在寄存器中,或者更糟,甚至不在缓存中,这会导致很长的内存提取
  • 从零减去该值求反,然后得到原始值和求反值的最大值

    • 优点:固定成本,因为不需要任何东西,比如面具
    • 缺点:如果条件理想,将始终比掩码方法慢,并且在使用
      maxps
      指令之前,我们必须等待
      subps
      完成
  • 与选项2类似,将原始值从零减去求反,然后使用
    和ps
    将结果与原始值“按位和”。我运行了一个测试,将其与方法2进行比较,它的行为似乎与方法2相同,除了处理
    NaN
    s时,在这种情况下,结果将与方法2的结果不同

    • 优点:应该比方法2略快,因为
      和ps
      通常比
      maxps
    • 缺点:当涉及
      NaN
      s时,这会导致任何意外行为吗?可能不是,因为一个
      NaN
      仍然是一个
      NaN
      ,即使它是
      NaN
      的不同值,对吗

  • 欢迎提出想法和意见。

    TL;DR:在几乎所有情况下,使用pcmpeq/shift生成掩码,并使用andps。它具有迄今为止最短的关键路径(与内存中的常量绑定),并且不能缓存未命中

    如何用内在论做到这一点 让编译器在未初始化的寄存器上发出
    pcmpeqd
    ,可能会很棘手。gcc/icc的最佳方式是

    __m128 abs_mask(void){
      // with clang, this turns into a 16B load,
      // with every calling function getting its own copy of the mask
      __m128i minus1 = _mm_set1_epi32(-1);
      return _mm_castsi128_ps(_mm_srli_epi32(minus1, 1));
    }
    // MSVC is BAD when inlining this into loops
    __m128 vecabs_and(__m128 v) {
      return _mm_and_ps(abs_mask(), v);
    }
    
    
    __m128 sumabs(const __m128 *a) { // quick and dirty no alignment checks
      __m128 sum = vecabs_and(*a);
      for (int i=1 ; i < 10000 ; i++) {
          // gcc, clang, and icc hoist the mask setup out of the loop after inlining
          // MSVC doesn't!
          sum = _mm_add_ps(sum, vecabs_and(a[i])); // one accumulator makes addps latency the bottleneck, not throughput
      }
      return sum;
    }
    
    可能您只需要将掩码作为16B常量存储在内存中。希望每个使用它的函数都不会重复。在32位代码中,将掩码放在内存常量中可能更有帮助,因为在32位代码中,只有8个XMM寄存器,因此
    veCAB
    可以在内存源操作数没有可用寄存器的情况下使用ANDPS来保持常量

    TODO:了解如何避免在常量内联的任何地方重复该常量。可能使用全局常量,而不是匿名
    set1
    ,会更好。但是您需要初始化它,但我不确定intrinsic是否可以作为全局
    \uuum128
    变量的初始化器。您希望它位于只读数据部分,而不是在程序启动时运行构造函数


    或者,使用

    __m128i minus1;  // undefined
    #if _MSC_VER && !__INTEL_COMPILER
    minus1 = _mm_setzero_si128();  // PXOR is cheaper than MSVC's silly load from the stack
    #endif
    minus1 = _mm_cmpeq_epi32(minus1, minus1);  // or use some other variable here, which will probably cost a mov insn without AVX, unless the variable is dead.
    const __m128 absmask = _mm_castsi128_ps(_mm_srli_epi32(minus1, 1));
    
    额外的PXOR非常便宜,但它仍然是uop,代码大小仍然是4字节。如果有人有更好的解决方案来克服MSVC不愿意发出我们想要的代码,请留下评论或编辑。但是,如果内联到一个循环中,这是不好的,因为pxor/pcmp/psrl都将在循环中

    使用
    movd
    加载32位常量并使用
    shufps
    进行广播可能还可以(不过,您可能需要手动将其从循环中取出)。这是3条指令(mov立即到GP reg、movd、shufps),在AMD上,movd速度较慢,在AMD上,矢量单元在两个整数核之间共享。(他们的超读版本。)


    选择最佳asm序列 好的,让我们看看这个,比如说通过天湖的Intel Sandybridge,还有一点提到Nehalem。有关我是如何解决这个问题的,请参阅微囊指南和说明时间。我还使用了Skylake的号码,有人在论坛上的帖子中链接了这些号码


    假设我们想要
    abs()
    的向量位于
    xmm0
    中,并且是FP代码典型的长依赖链的一部分

    因此,让我们假设任何不依赖于
    xmm0
    的操作都可以在
    xmm0
    准备就绪之前开始几个周期。我已经测试过了,如果内存操作数的地址不是dep链的一部分(即,不是关键路径的一部分),那么带有内存操作数的指令不会给依赖链增加额外的延迟


    我不完全清楚,当它是微熔合uop的一部分时,内存操作可以在多早开始。据我所知,重新订购缓冲区(ROB)与融合的UOP一起工作,并跟踪UOP从发行到报废(168(SnB)到224(SKL)条目)。还有一个调度器在未使用的域中工作,只保存输入操作数就绪但尚未执行的UOP。uop可以在解码(或从uop缓存加载)的同时发送到ROB(融合)和调度程序(未融合),在天湖有97个

    还有人说Skylake每个时钟处理6个UOP。据我所知,Skylake将每个时钟将整个uop缓存线(最多6个uop)读入uop缓存和ROB之间的缓冲区。进入ROB/调度程序的问题仍然是4-wide。(即使
    nop
    仍然是每个时钟4个)。这个缓冲区有帮助 何处导致以前的Sandybridge Microach设计出现瓶颈。我以前以为这个“问题队列”就是这个缓冲区,但显然不是

    无论它如何工作,如果地址不在关键路径上,调度程序都足够大,可以及时从缓存中获取数据


    1a:带内存操作数的掩码
    • 字节:7个insn,16个数据。(平均值:8英寸)
    • 融合域uops:1*n
    • 添加到关键路径的延迟:1c(假设一级缓存命中)
    • 吞吐量:1/c。(受2个负载/立方英尺的限制)
    • 当insn发出时,如果
      xmm0
      已准备就绪,“延迟”:一级缓存命中时约4c

    1b:来自寄存器的掩码
    • 字节:10 insn+16数据。(AVX:12 insn字节)
    • 融合域uops:1+1*n
    • 潜伏期
      __m128i minus1;  // undefined
      #if _MSC_VER && !__INTEL_COMPILER
      minus1 = _mm_setzero_si128();  // PXOR is cheaper than MSVC's silly load from the stack
      #endif
      minus1 = _mm_cmpeq_epi32(minus1, minus1);  // or use some other variable here, which will probably cost a mov insn without AVX, unless the variable is dead.
      const __m128 absmask = _mm_castsi128_ps(_mm_srli_epi32(minus1, 1));
      
      ANDPS  xmm0, [mask]  # in the loop
      
      movaps   xmm5, [mask]   # outside the loop
      
      ANDPS    xmm0, xmm5     # in a loop
      # or PAND   xmm0, xmm5    # higher latency, but more throughput on Nehalem to Broadwell
      
      # or with an inverted mask, if set1_epi32(0x80000000) is useful for something else in your loop:
      VANDNPS   xmm0, xmm5, xmm0   # It's the dest that's NOTted, so non-AVX would need an extra movaps
      
      # outside a loop
      PCMPEQD  xmm5, xmm5  # set to 0xff...  Recognized as independent of the old value of xmm5, but still takes an execution port (p1/p5).
      PSRLD    xmm5, 1     # 0x7fff...  # port0
      # or PSLLD xmm5, 31  # 0x8000...  to set up for ANDNPS
      
      ANDPS    xmm0, xmm5  # in the loop.  # port5
      
      VXORPS  xmm5, xmm5, xmm5   # outside the loop
      
      VSUBPS  xmm1, xmm5, xmm0   # inside the loop
      VMAXPS  xmm0, xmm0, xmm1
      
      # inside the loop
      XORPS  xmm1, xmm1   # not on the critical path, and doesn't even take an execution unit on SnB and later
      SUBPS  xmm1, xmm0
      MAXPS  xmm0, xmm1
      
      VXORPS  xmm5, xmm5, xmm5   # outside the loop.  Without AVX: zero xmm1 inside the loop
      
      VSUBPS  xmm1, xmm5, xmm0   # inside the loop
      VANDPS  xmm0, xmm0, xmm1
      
      PSLLD  xmm0, 1
      PSRLD  xmm0, 1