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

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了使用SSE计算绝对值的最快方法相关的知识,希望对你有一定的参考价值。

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

  1. 使用andpsandnotps掩盖符号位。 优点:一个快速指令,如果掩码已经在寄存器中,这使得它非常适合在循环中多次执行此操作。 缺点:掩码可能不在寄存器中或更糟糕,甚至不在缓存中,导致非常长的内存提取。
  2. 将值从零减去否定,然后得到原始的最大值并取消。 优点:固定成本,因为无需取物,就像面具一样。 缺点:如果条件理想,将始终比掩码方法慢,并且我们必须等待subps完成才能使用maxps指令。
  3. 与选项2类似,将原始值从零减去否定,然后使用andps将结果与“原位”和“原点”相加。我运行了一个测试,将其与方法2进行比较,除了处理NaNs时,它似乎与方法2的行为相同,在这种情况下,结果将是与方法2的结果不同的NaN。 优点:应该比方法2略快,因为andps通常比maxps快。 缺点:当涉及到NaNs时,这会导致任何意外行为吗?也许不是,因为NaN仍然是NaN,即使它是NaN的不同值,对吧?

欢迎提出想法和意见。

答案

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

How to do that with intrinsics

让编译器在未初始化的寄存器上发出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;
}

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));

可能你只是将掩码存储在内存中作为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));

额外的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's微型指南和指导时间,了解我如何解决这个问题。我还使用了在http://realwordtech.com/论坛上发帖链接的Skylake号码。


让我们说我们想要abs()的向量是在xmm0中,并且是FP代码典型的长依赖链的一部分。

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


我不完全清楚记忆操作在它是微融合uop的一部分时的早期开始。根据我的理解,重新排序缓冲区(ROB)与融合的uops一起工作,并跟踪从发布到退役(168(SnB)到224(SKL)条目)的uops。还有一个在未融合域中工作的调度程序,只保留已准备好但尚未执行的输入操作数的微指令。 uops可以在解码(或从uop缓存加载)的同时发出到ROB(融合)和调度程序(unfused)。 If I'm understanding this correctly, it's 54 to 64 entries in Sandybridge to Broadwell和Skylake的97。 There's some unfounded speculation about it not being a unified (ALU/load-store) scheduler anymore

还有人谈到Skylake每时钟处理6次uop。据我所知,Skylake会将每个时钟的整个uop-cache行(最多6个uop)读入uop缓存和ROB之间的缓冲区。进入ROB /调度程序的问题仍然是4个问题。 (甚至nop仍然是每时钟4)。这个缓冲区有助于code alignment / uop cache line boundaries在以前的Sandybridge-microarch设计中造成瓶颈。我以前认为这个“问题队列”是这个缓冲区,但显然它不是。

但是,如果地址不在关键路径上,则调度程序足够大,可以及时准备好缓存中的数据。


1a: mask with a memory operand

ANDPS  xmm0, [mask]  # in the loop
  • 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: mask from a register

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

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


1c: generate the mask on the fly:

# 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
  • 字节: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准备就绪的“站立开始”开始的de-chain链情况下,它只有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
  • 字节: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 latency is down to 4 (from 5 in *well)。 x87 FADD仍然是3周期延迟,所以仍然有一个3周期标量80bit-FP加法器,但只在一个端口上。

2b: same but without AVX:

# insi

以上是关于使用SSE计算绝对值的最快方法的主要内容,如果未能解决你的问题,请参考以下文章

进行水平 SSE 向量求和(或其他缩减)的最快方法

在整数 SSE 寄存器中移动更高或更低 64 位的最快方法

用 SSE 在 C++ 中将两个 32 位整数向量相乘的最快方法

如何使用 SSE 指令集绝对 2 个双精度或 4 个浮点数? (最高 SSE4)

此函数如何通过 NOT 和 AND 运算计算浮点数的绝对值?

使用 Scala 根据 RDD 中的多个键列对值进行分组的最快方法是啥? [复制]