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

Kum*_*ter 14 x86 sse simd vectorization absolute-value

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

  1. 使用andps或屏蔽符号位andnotps.

    • 优点:一个快速指令,如果掩码已经在寄存器中,这使得它非常适合在循环中多次执行此操作.
    • 缺点:掩码可能不在寄存器中或更糟糕,甚至不在缓存中,导致非常长的内存提取.
  2. 将值从零减去否定,然后得到原始的最大值并否定.

    • 优点:固定成本,因为无需取物,就像面具一样.
    • 缺点:如果条件理想,将始终比掩码方法慢,并且我们必须等待subps完成才能使用该maxps指令.
  3. 与选项2类似,将原始值从零减去否定,但随后使用原始值"按位"和"按位" andps.我运行了一个测试,将其与方法2进行比较,除了处理NaNs 之外,它似乎与方法2的行为相同,在这种情况下,结果将NaN与方法2的结果不同.

    • 优点:应该比方法2略快,因为andps通常比速度快maxps.
    • 缺点:当NaN涉及到s 时,这是否会导致任何意外行为?也许不是,因为a NaN仍然是a NaN,即使它是一个不同的值NaN,对吧?

欢迎提出想法和意见.

Pet*_*des 34

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

如何使用内在函数来做到这一点

让编译器pcmpeqd在未初始化的寄存器上发射可能很棘手.(godbolt).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;
}
Run Code Online (Sandbox Code Playgroud)

clang 3.5及更高版本"优化"set1/shift以从内存加载常量.但它会pcmpeqd用来实现set1_epi32(-1). TODO:找到一系列内在函数,用clang生成所需的机器代码.从内存加载常量不是性能灾难,但让每个函数使用不同的掩码副本是非常可怕的.

MSVC:VS2013:

  • _mm_uninitialized_si128() 没有定义.

  • _mm_cmpeq_epi32(self,self)对于未初始化的变量,将movdqa xmm, [ebp-10h]在此测试用例中发出一个(即从堆栈中加载一些未初始化的数据.这样可以减少缓存未命中的风险,而不仅仅是从内存中加载最终的常量.但是,Kumputer说MSVC无法提升pcmpeqd/psrld离开循环(我假设在内联时vecabs),所以这是不可用的,除非你手动内联并自行提升循环中的常量.

  • _mm_srli_epi32(_mm_set1_epi32(-1), 1)在movdqa中使用结果来加载所有-1的向量(在循环外部提升)和psrld循环内部.所以这太可怕了.如果您要加载16B常量,它应该是最终的向量.每个循环迭代生成掩码的整数指令也很可怕.

对MSVC的建议:放弃动态生成掩码,然后写

const __m128 absmask = _mm_castsi128_ps(_mm_set1_epi32(~(1<<31));
Run Code Online (Sandbox Code Playgroud)

可能你只是将掩码存储在内存中作为16B常量.希望不会为使用它的每个功能重复.将掩码放在存储器常量中更有可能在32位代码中有用,在32位代码中你只有8个XMM寄存器,所以vecabs如果它没有一个可以保持常量的寄存器,那么只能使用内存源操作数进行ANDPS.

TODO:找出如何避免在内联的每个地方重复常量.可能使用全局常量而不是匿名set1会很好.但是你需要初始化它,但我不确定内在函数是否作为全局__m128变量的初始值.您希望它进入只读数据部分,而不是在程序启动时运行的构造函数.


或者,使用

__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));
Run Code Online (Sandbox Code Playgroud)

额外的PXOR非常便宜,但它仍然是一个uop,代码大小仍然是4个字节.如果有人有任何更好的解决方案来克服MSVC不愿意发出我们想要的代码,请留下评论或编辑.但是,如果内联到循环中,这是不好的,因为pxor/pcmp/psrl都将在循环内部.

使用movd和广播加载32位常数shufps可能没问题(同样,你可能不得不手动将其从循环中提升).这是3个指令(对于GP reg,movd,shufps是mov-immediate),而且在两个整数核心之间共享向量单元的AMD上,movd很慢.(他们的超线程版本.)


选择最好的asm序列

好吧,让我们看看这个,让我们通过Skylake说英特尔Sandybridge,稍微提一下Nehalem.请参阅Agner Fog的微型指南和指导时间,了解我如何解决这个问题.我还在http://realwordtech.com/论坛的帖子中使用了某人链接的Skylake号码.


比方说,我们要在载体abs()xmm0,并且是一个长期依赖链的一部分,是典型的FP码.

因此,假设任何不依赖的操作都xmm0可以在xmm0准备好之前开始几个周期.我已经测试过,并且内存操作数的指令不会给依赖链增加额外的延迟,假设内存操作数的地址不是dep链的一部分(即不是关键路径的一部分).


我不完全清楚记忆操作在它是微融合uop的一部分时的早期开始.根据我的理解,重新排序缓冲区(ROB)与融合的uops一起工作,并跟踪从发布到退役(168(SnB)到224(SKL)条目)的uops.还有一个在未融合域中工作的调度程序,只保留已准备好但尚未执行的输入操作数的微指令.uops可以在解码(或从uop缓存加载)的同时发出到ROB(融合)和调度程序(unfused). 如果我理解这一点,那么Sandybridge到Broadwell的54到64个条目,以及Skylake的97 个条目. 关于它不再是一个统一的(ALU /加载存储)调度程序,有一些毫无根据的猜测.

还有人谈到Skylake每时钟处理6次uop.据我所知,Skylake会将每个时钟的整个uop-cache行(最多6个uop)读入uop缓存和ROB之间的缓冲区.进入ROB /调度程序的问题仍然是4个问题.(即使nop每个时钟仍然是4).此缓冲区有助于代码对齐/ uop缓存行边界导致以前Sandybridge-microarch设计的瓶颈.我以前认为这个"问题队列"是这个缓冲区,但显然它不是.

然而,它的工作原理,调度足够大,以获得从缓存中的数据的时间准备,如果地址不在关键路径上.


1a:具有内存操作数的掩码

ANDPS  xmm0, [mask]  # in the loop
Run Code Online (Sandbox Code Playgroud)
  • bytes:7 insn,16 data.(AVX:8 insn)
  • 融合域uops:1*n
  • 延迟添加到关键路径:1c(假设L1缓存命中)
  • 吞吐量:1/c. (Skylake:2/c)(限制为2次/ c)
  • "延迟"如果xmm0在这个insn发布时已经准备就绪:在L1缓存命中时~4c.

1b:来自寄存器的掩码

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
Run Code Online (Sandbox Code Playgroud)
  • bytes:10 insn + 16 data.(AVX:12个insn字节)
  • 融合域uops:1 + 1*n
  • 延迟添加到dep链:1c(在循环的早期具有相同的cache-miss警告)
  • 吞吐量:1/c. (Skylake:3/c)

PANDNehalem到Broadwell的吞吐量为3/c,但延迟= 3c(如果在两个FP域操作之间使用,在Nehalem上更差).我想只有port5具有将按位操作直接转发到其他FP执行单元(前Skylake)的接线.Pre-Nehalem,在AMD上,按位FP操作与整数FP操作相同,因此它们可以在所有端口上运行,但具有转发延迟.


1c:动态生成掩码:

# 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
Run Code Online (Sandbox Code Playgroud)
  • 字节:12(AVX:13)
  • 融合域uops:2 + 1*n(无内存操作)
  • 延迟添加到dep链:1c
  • 吞吐量:1/c. (Skylake:3/c)
  • 所有3个uop的吞吐量:1/c使所有3个向量ALU端口饱和
  • "延迟"如果xmm0在此序列发出时准备就绪(无循环):3c(如果ANDPS必须等待整数数据准备就绪,则SnB/IvB上可能有1c旁路延迟.Agner Fog说在某些情况下整数没有额外的延迟 - > SnB/IvB上的FP-boolean.)

此版本的内存仍然比内存中具有16B常量的版本少.它也适用于不经常调用的函数,因为没有负载会导致缓存未命中.

"旁路延迟"应该不是问题.如果xmm0是长依赖链的一部分,则掩码生成指令将提前执行​​,因此xmm5中的整数结果将有时间在xmm0准备好之前达到ANDPS,即使它采用慢速通道.

根据Agner Fog的测试,Haswell没有整数结果的旁路延迟 - > FP boolean.他对SnB/IvB的描述说这是一些整数指令输出的情况.因此,即使xmm0是在这个指令序列发出时已经准备就绪的"站起来"开始的一个分段链的情况下,它只有3c on*well,4c on*Bridge.如果执行单元正在清除积压的uop,那么延迟可能并不重要.

无论哪种方式,ANDPS的输出都将在FP域中,并且如果在其中使用,则没有旁路延迟MULPS.

在Nehalem,绕行延误是2c.因此,在Nehalem的dep链开始时(例如在分支错误预测或I $ miss之后),如果xmm0此序列发出的是5c ,则"等待时间" 已准备就绪.如果你非常关心Nehalem,并且期望这个代码是频繁的分支错误预测或类似的管道停顿之后运行的第一件事,这使得OoOE机器在xmm0准备好之前无法开始计算掩码,那么这可能不是非循环情况的最佳选择.


2a:AVX max(x,0-x)

VXORPS  xmm5, xmm5, xmm5   # outside the loop

VSUBPS  xmm1, xmm5, xmm0   # inside the loop
VMAXPS  xmm0, xmm0, xmm1
Run Code Online (Sandbox Code Playgroud)
  • 字节:AVX:12
  • 融合域uops:1 + 2*n(无内存操作)
  • 延迟添加到dep链:6c(Skylake:8c)
  • 吞吐量:每2c 1个(两个port1 uops).(Skylake:1/c,假设MAXPS使用相同的两个端口SUBPS.)

Skylake删除了单独的vector-FP add单元,并在端口0和1上的FMA单元中添加了向量.这使FP增加了一倍的吞吐量,代价是1c更多的延迟.的FMA延迟降低到4(从5*孔).x87 FADD仍然是3个周期的延迟,所以仍然有一个3周期标量80bit-FP加法器,但只在一个端口上.

2b:相同但没有AVX:

# 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
Run Code Online (Sandbox Code Playgroud)
  • 字节:9
  • 融合域uops:3*n(无内存操作)
  • 延迟添加到dep链:6c(Skylake:8c)
  • 吞吐量:每2c 1个(两个port1 uops).(Skylake:1/c)
  • 如果xmm0在发出此序列时没有准备好"延迟" (无循环):相同

xorps same,same在Sandbridge系列微体系结构的寄存器重命名期间处理具有处理器识别(如)的归零的寄存器归零,并且具有零延迟和4/c的吞吐量.(与reg-> reg相同,IvyBridge以后可以消除.)

但它并不是免费的:它仍然需要融合域中的uop,所以如果你的代码只受到4uop /循环发布率的瓶颈,这将减慢你的速度.超线程更有可能发生这种情况.


3:ANDPS(x,0-x)

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
Run Code Online (Sandbox Code Playgroud)
  • bytes:AVX:12 non-AVX:9
  • 融合域uops:1 + 2*n(无内存操作).(没有AVX:3*n)
  • 延迟添加到dep链:4c(Skylake:5c)
  • 吞吐量:1/c(饱和p1和p5).Skylake:3/2c:(3个向量uop /周期)/(uop_p01 + uop_p015).
  • 如果xmm0在发出此序列时没有准备好"延迟" (无循环):相同

这应该有效,但IDK或NaN会发生什么.很好地观察到ANDPS的延迟较低,并且不需要FPU添加端口.

这是非AVX的最小尺寸.


4:左/右移位:

PSLLD  xmm0, 1
PSRLD  xmm0, 1
Run Code Online (Sandbox Code Playgroud)
  • 字节:10(AVX:10)
  • 融合域uops:2*n
  • 延迟添加到dep链:4c(2c +旁路延迟)
  • 吞吐量:1/2c(饱和p0,也由FP mul使用).(Skylake 1/c:矢量移位吞吐量加倍)
  • 如果xmm0在发出此序列时没有准备好"延迟" (无循环):相同

    这是AVX中最小的(以字节为单位).

    这有可能无法保留寄存器,并且不会在循环中使用.(循环中没有备用的备用,概率使用andps xmm0, [mask]).

我假设从FP到整数移位有1c旁路延迟,然后在返回途中再有1c,所以这和SUBPS/ANDPS一样慢.它确实保存了一个无执行端口的uop,因此如果融合域uop吞吐量是一个问题,它就有优势,并且你不能将掩码生成拉出循环.(例如,因为这是一个在循环中调用的函数,而不是内联函数).


何时使用:从内存加载掩码使代码简单,但存在缓存未命中的风险.并占用16B的ro数据而不是9个指令字节.

  • 循环中需要:1c:在循环外生成掩码(使用pcmp/shift); 使用单个andps内部.如果您不能将寄存器丢弃,请将其溢出到堆栈中并且1a : andps xmm0, [rsp + mask_local]. (生成和存储不太可能导致高速缓存未命中而不是常量).只有在关键路径中增加1个周期,在循环内部使用1个单uop指令.它是一个port5 uop,所以如果你的循环使shuffle端口饱和并且没有延迟限制,那PAND可能会更好.(SnB/IvB在p1/p5上有单位,但Haswell/Broadwell/Skylake只能在p5上进行随机播放.Skylake确实提高了吞吐量(V)(P)BLENDV,但不是其他shuffle-port操作.如果AIDA数字是正确的,非AVX BLENDV是1c lat~3/c tput,但AVX BLENDV是2c lat,1/c tput(仍然是Haswell的输出改进))

  • 在频繁调用的非循环函数中需要一次(因此您不能在多次使用时分摊掩码生成):

    1. 如果uop吞吐量是一个问题:1a : andps xmm0, [mask]. 偶尔的缓存缺失应该通过uops的节省来摊销,如果真的是瓶颈的话.
    2. 如果延迟不是问题(该函数仅用作短的非循环携带的dep链的一部分,例如arr[i] = abs(2.0 + arr[i]);),并且你想要避免内存中的常量:4,因为它只有2 uops.如果abs来自dep链的开始或结束,则不会有从负载或商店的旁路延迟.
    3. 如果uop吞吐量不是问题:1c:使用整数动态生成pcmpeq / shift.没有可能的高速缓存未命中,并且仅向关键路径添加1c.
  • 在一个不经常调用的函数中需要(在任何循环之外):只是优化大小(小版本都不使用内存中的常量).非AVX:3.AVX:4.他们不坏,不能缓存.关键路径的4周期延迟比版本1c更糟,所以如果你不认为3个指令字节是一个大问题,那就选择1c.当性能不重要时,版本4对于注册压力情况很有意义,并且您希望避免任何溢出.


  • AMD CPU:有一个旁路延迟到/从ANDPS(它本身有2c延迟),但我认为它仍然是最好的选择.它仍然超过5-6周期延迟SUBPS. MAXPS是2c延迟.由于Bulldozer系列CPU上的FP操作具有较高的延迟,因此您更有可能无序执行,以便及时生成掩码,以便在其他操作数到达时准备就绪ANDPS.我猜测Bulldozer通过Steamroller没有单独的FP添加单元,而是在FMA单元中进行向量加法和乘法运算. 在AMD Bulldozer系列CPU上,3总是不错的选择. 在这种情况下,2看起来更好,因为从fma域到fp域和返回的旁路延迟更短.请参阅Agner Fog的微型指南,15.11不同执行域之间的数据延迟).

  • Silvermont:与SnB类似的延迟.仍然用1c for循环,和prob.也适合一次性使用.Silvermont是无序的,所以它可以提前准备好掩模,仍然只能在关键路径上增加1个循环.