19

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

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

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

    • 优点:固定成本,因为不需要像面具一样获取任何东西。
    • 缺点:如果条件理想,总是会比掩码方法慢,并且我们必须等待subps完成才能使用maxps指令。
  3. 与选项 2 类似,将原始值从零减去以求反,然后使用andps. 我进行了一个测试,将其与方法 2 进行比较,它的行为似乎与方法 2 相同,除了在处理NaNs 时,在这种情况下,结果将NaN与方法 2 的结果不同。

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

欢迎提出想法和意见。

4

1 回答 1

39

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

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 中使用results 来加载所有 -1 的向量(在循环外提升)和psrld循环内的 a。所以这完全是可怕的。如果你要加载一个 16B 的常量,它应该是最终的向量。让整数指令在每次循环迭代时生成掩码也很可怕。

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

const __m128 absmask = _mm_castsi128_ps(_mm_set1_epi32(~(1<<31));

可能您只会将掩码作为 16B 常量存储在内存中。希望不要为使用它的每个功能重复。将掩码放在内存常量中更有可能在 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 都将在循环内。

加载一个 32 位常量movd并使用广播shufps可能没问题(同样,您可能必须手动将其从循环中提升出来)。那是 3 条指令(mov-immediate to a GP reg, movd, shufps),并且 movd 在 AMD 上很慢,其中向量单元在两个整数内核之间共享。(他们的超线程版本。)


选择最佳的 asm 序列

好的,让我们看看这个,让我们说通过 Skylake 的 Intel Sandybridge,并稍微提到 Nehalem。请参阅Agner Fog 的微架构指南和说明时间,了解我是如何解决这个问题的。我还使用了某人在http://realwordtech.com/论坛上的帖子中链接的 Skylake 号码。


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

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


当它是微融合 uop 的一部分时,我并不完全清楚内存操作可以多早开始。据我了解,重新排序缓冲区(ROB)与融合的微指令一起使用,并跟踪微指令从发布到退休(168(SnB)到 224(SKL)条目)。还有一个在未融合域中工作的调度程序,仅保存输入操作数已准备好但尚未执行的微指令。uop 可以在解码(或从 uop 缓存加载)时同时发送到 ROB(已融合)和调度程序(未融合)。 如果我理解正确的话,从 Sandybridge 到 Broadwell 有 54 到 64 个条目,在 Skylake 有 97 个条目。 关于它不再是一个统一的(ALU/load-store)调度程序有一些毫无根据的猜测

也有人谈论 Skylake 每时钟处理 6 微秒。据我了解,Skylake 将每个时钟将整个 uop 缓存行(最多 6 个 uops)读取到 uop 缓存和 ROB 之间的缓冲区中。进入 ROB/调度程序的问题仍然是 4-wide。(甚至nop仍然是每时钟 4 个)。此缓冲区有助于代码对齐/uop 缓存线边界导致先前 Sandybridge 微架构设计的瓶颈。我以前认为这个“问题队列”就是这个缓冲区,但显然不是。

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


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

ANDPS  xmm0, [mask]  # in the loop
  • 字节:7 个 insn,16 个数据。(AVX:8 插曲)
  • 融合域微指令: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
  • 字节:10 insn + 16 数据。(AVX:12 个 insn 字节)
  • 融合域微指令:1 + 1*n
  • 延迟添加到 dep 链:1c(在循环的早期使用相同的缓存未命中警告)
  • 吞吐量:1/c。 (Skylake: 3/c)

PAND是 Nehalem 到 Broadwell 的吞吐量 3/c,但延迟 = 3c(如果在两个 FP 域操作之间使用,在 Nehalem 上甚至更糟)。我猜只有端口 5 具有将按位运算直接转发到其他 FP 执行单元(Skylake 之前)的接线。在 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
  • 字节:12(AVX:13)
  • 融合域微操作:2 + 1*n(无内存操作)
  • 延迟添加到 dep 链:1c
  • 吞吐量:1/c。 (Skylake: 3/c)
  • 所有 3 个微指令的吞吐量:1/c 饱和所有 3 个矢量 ALU 端口
  • “延迟”如果xmm0在此序列发出时已准备好(无循环):3c(如果 ANDPS 必须等待整数数据准备好,则 SnB/IvB 上可能会出现 +1c 旁路延迟。Agner Fog 说在某些情况下整数没有额外延迟-> SnB/IvB 上的 FP-布尔值。)

这个版本仍然比内存中具有 16B 常量的版本占用更少的内存。它也非常适合不常调用的函数,因为没有负载会遭受缓存未命中。

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

根据 Agner Fog 的测试,Haswell 对整数结果没有绕过延迟 -> FP 布尔值。他对 SnB/IvB 的描述表明,某些整数指令的输出就是这种情况。因此,即使在该指令序列发出时已准备好的“站立启动”开始链的情况下,xmm0*well 上只有 3c,*Bridge 上只有 4c。如果执行单元清除微指令的积压与发出它们的速度一样快,延迟可能并不重要。

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

在 Nehalem 上,旁路延迟为 2c。因此,在 Nehalem 上的 dep 链开始时(例如,在分支错误预测或 I$ 未命中之后),“延迟”如果xmm0在此序列发出时已准备好为 5c。如果您非常关心 Nehalem,并期望此代码是在频繁的分支错误预测或类似的管道停顿导致 OoOE 机器在xmm0准备好之前无法开始计算掩码之后运行的第一件事,那么这可能不是非循环情况的最佳选择。


2a:AVX 最大值(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 丢弃了单独的向量-FP 添加单元,并在端口 0 和 1 上的 FMA 单元中进行向量添加。这使 FP 添加吞吐量翻了一番,但延迟增加了 1c。FMA 延迟降至 4(从 5 in *well)。x87FADD仍然是 3 周期延迟,因此仍然有一个 3 周期标量 80 位 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
  • 字节:9
  • 融合域微操作:3*n(无内存操作)
  • 延迟添加到 dep 链:6c(Skylake:8c)
  • 吞吐量:每 2c 1 个(两个 port1 uops)。(天湖:1/c)
  • “延迟”如果xmm0在此序列发出时已准备好(无循环):相同

在 Sandbridge 系列微架构上的寄存器重命名期间处理使用处理器识别的归零习惯(如xorps same,same)对寄存器进行归零,并且具有零延迟和 4/c 的吞吐量。(与 IvyBridge 和更高版本可以消除的 reg->reg 移动相同。)

但它不是免费的:它仍然需要在融合域中使用 uop,因此如果您的代码仅受到 4uop/cycle 发布率的瓶颈,这会减慢您的速度。超线程更有可能发生这种情况。


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

这应该可行,但 IDK 要么发生 NaN。很好地观察到 ANDPS 的延迟较低并且不需要 FPU 添加端口。

这是非 AVX 的最小尺寸。


4:左移/右移:

PSLLD  xmm0, 1
PSRLD  xmm0, 1
  • 字节:10(AVX:10)
  • 融合域微指令: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-data 而不是 9 个指令字节。

  • 循环中需要:1c:在循环外生成掩码(使用 pcmp/shift);使用单个andps内部。如果您无法保留寄存器,请将其溢出到堆栈和1a : andps xmm0, [rsp + mask_local]。(生成和存储比常量更不可能导致缓存未命中)。仅向关键路径添加 1 个周期,循环内有 1 个单 uop 指令。这是一个 port5 uop,所以如果你的循环使随机端口饱和并且不受延迟限制,PAND可能会更好。(SnB/IvB 在 p1/p5 上有洗牌单元,但 Haswell/Broadwell/Skylake 只能在 p5 上洗牌。Skylake 确实增加了(V)(P)BLENDV,但不是其他随机端口操作。如果 AIDA 数字正确,非 AVX BLENDV 是 1c lat ~3/c tput,但 AVX BLENDV 是 2c lat,1/c tput(仍然比 Haswell 的 tput 改进)

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

    1. 如果 uop 吞吐量是一个问题:1a : andps xmm0, [mask]。如果这确实是瓶颈,那么偶尔的缓存未命中应该在 uops 的节省上分摊。
    2. 如果延迟不是问题(该函数仅用作短的非循环承载的 dep 链的一部分,例如arr[i] = abs(2.0 + arr[i]);),并且您希望避免内存中的常量:4,因为它只有 2 个微指令。如果abs出现在 dep 链的开头或结尾,则不会有从加载或到存储的旁路延迟。
    3. 如果 uop 吞吐量不是问题:1c:使用 integer 即时生成pcmpeq / shift。不可能有缓存未命中,并且只会将 1c 添加到关键路径。
  • 在一个不经常调用的函数中需要(在任何循环之外):只需优化大小(两个小版本都不使用内存中的常量)。非AVX:3。AVX:4。它们还不错,并且不能缓存未命中。关键路径的 4 个周期延迟比 1c 版本更糟糕,因此如果您认为 3 个指令字节不是什么大问题,请选择1c第4版适用于性能不重要且您希望避免溢出任何内容的寄存器压力情况。


  • AMD CPU:有一个往返延迟ANDPS(它本身有 2c 延迟),但我认为它仍然是最佳选择。它仍然超过了 5-6 个周期的延迟SUBPSMAXPS是 2c 延迟。由于 Bulldozer 系列 CPU 上 FP 操作的高延迟,乱序执行更有可能及时生成掩码,以便在另一个操作数出现时做好准备ANDPS。我猜推土机通过 Steamroller 没有单独的 FP 加法单元,而是在 FMA 单元中进行矢量加法和乘法运算。 对于 AMD Bulldozer 系列 CPU, 3将永远是一个糟糕的选择。 在这种情况下, 2看起来更好,因为从 fma 域到 fp 域并返回的旁路延迟更短。请参阅 Agner Fog 的微架构指南,15.11 不同执行域之间的数据延迟)。

  • Silvermont:与 SnB 类似的延迟。仍然使用1c for 循环和概率。也可一次性使用。Silvermont 是无序的,因此它可以提前准备好掩码,仍然只为关键路径增加 1 个周期。

于 2015-09-06T10:20:38.687 回答