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

问题描述 投票:13回答:1

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

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

欢迎提出想法和意见。

x86 vectorization sse simd absolute-value
1个回答
33
投票

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:

# 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
  • 字节:9
  • 融合域uops:3 * n(无内存操作)
  • 延迟添加到dep链:6c(Skylake:8c)
  • 吞吐量:每2c 1个(两个port1 uops)。 (Skylake:1 / c)
  • “延迟”,如果xmm0准备就绪,这个序列发出(没有循环):相同

在Sandbridge系列微体系结构的寄存器重命名期间处理具有处理器识别的归零惯用程序(如xorps same,same)的寄存器归零,并且具有零延迟和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
  • 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: shift left/right:

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

  • AMD CPU:来自ANDPS的旁路延迟(本身有2c延迟),但我认为它仍然是最好的选择。它仍然胜过SUBPS的5-6周期潜伏期。 MAXPS是2c延迟。由于Bulldozer系列CPU上的FP操作的延迟很高,你甚至更有可能无序执行,以便能够及时生成掩码,以便在ANDPS的另一个操作数为时准备就绪。 。我猜测Bulldozer通过Steamroller没有单独的FP添加单元,而是在FMA单元中进行向量加法和乘法运算。在AMD Bulldozer系列CPU上,3总是不错的选择。在这种情况下,2看起来更好,因为从fma域到fp域和返回的旁路延迟更短。请参阅Agner Fog的微型指南,第182页(15.11不同执行域之间的数据延迟)。
  • Silvermont:与SnB类似的延迟。仍然用1c for循环,和prob。也适合一次性使用。 Silvermont是无序的,所以它可以提前准备好掩模,仍然只能在关键路径上增加1个循环。
© www.soinside.com 2019 - 2024. All rights reserved.