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
我假设从 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 改进)
在经常调用的非循环函数中需要一次(因此您不能在多次使用时分摊掩码生成):
- 如果 uop 吞吐量是一个问题:1a :
andps xmm0, [mask]
。如果这确实是瓶颈,那么偶尔的缓存未命中应该在 uops 的节省上分摊。
- 如果延迟不是问题(该函数仅用作短的非循环承载的 dep 链的一部分,例如
arr[i] = abs(2.0 + arr[i]);
),并且您希望避免内存中的常量:4,因为它只有 2 个微指令。如果abs
出现在 dep 链的开头或结尾,则不会有从加载或到存储的旁路延迟。
- 如果 uop 吞吐量不是问题:1c:使用 integer 即时生成
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
。我猜推土机通过 Steamroller 没有单独的 FP 加法单元,而是在 FMA 单元中进行矢量加法和乘法运算。 对于 AMD Bulldozer 系列 CPU, 3将永远是一个糟糕的选择。 在这种情况下, 2看起来更好,因为从 fma 域到 fp 域并返回的旁路延迟更短。请参阅 Agner Fog 的微架构指南,15.11 不同执行域之间的数据延迟)。
Silvermont:与 SnB 类似的延迟。仍然使用1c for 循环和概率。也可一次性使用。Silvermont 是无序的,因此它可以提前准备好掩码,仍然只为关键路径增加 1 个周期。