TL;DR:在几乎所有情况下,使用 pcmpeq/shift 生成掩码,然后使用 andps 来使用它。 它具有迄今为止最短的关键路径(与 constant-from-memory ),并且不能缓存未命中。
如何用内在函数做到这一点
让编译器在未初始化的寄存器上发出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 位代码中有所帮助,其中您只有 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 条指令(mov-immediate to a GP reg, movd, shufps),并且 movd 在 AMD 上很慢,其中向量单元在两个整数内核之间共享。 (他们的超线程版本。)
选择最佳的 asm 序列
好的,让我们看看这个,让我们说通过 Skylake 的 Intel Sandybridge,并稍微提到 Nehalem。请参阅Agner Fog's microarch 指南和说明时间,了解我是如何解决这个问题的。我还使用了某人在 http://realwordtech.com/ 论坛上的帖子中链接的 Skylake 号码。
假设我们想要 abs() 的向量在 xmm0 中,并且是 FP 代码典型的长依赖链的一部分。
所以让我们假设任何不依赖于xmm0 的操作可以在xmm0 准备好之前开始几个周期。我已经测试过,假设内存操作数的地址不是 dep 链的一部分(即不是关键路径的一部分),带有内存操作数的指令不会给依赖链增加额外的延迟。
当它是微融合 uop 的一部分时,我不完全清楚内存操作可以多早开始。据我了解,重新排序缓冲区(ROB)与融合的微指令一起使用,并跟踪微指令从发布到退休(168(SnB)到 224(SKL)条目)。还有一个在未融合域中工作的调度程序,仅保存输入操作数已准备好但尚未执行的微指令。 uop 可以在解码(或从 uop 缓存加载)时同时发送到 ROB(已融合)和调度程序(未融合)。 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 微秒。据我了解,Skylake 将每个时钟将整个 uop 缓存行(最多 6 个 uops)读取到 uop 缓存和 ROB 之间的缓冲区中。进入 ROB/调度程序的问题仍然是 4-wide。 (即使nop 仍然是每时钟 4 个)。这个缓冲区有助于
其中code alignment / uop cache line boundaries 导致以前的 Sandybridge-microarch 设计出现瓶颈。我之前认为这个“问题队列”就是这个缓冲区,但显然不是。
不管它如何工作,如果地址不在关键路径上,调度程序足够大,可以及时从缓存中获取数据。
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 上的 integer->FP-boolean 没有额外的延迟。)
这个版本仍然比内存中具有 16B 常量的版本占用更少的内存。它也非常适合不常调用的函数,因为没有负载会遭受缓存未命中。
“旁路延迟”应该不是问题。如果 xmm0 是长依赖链的一部分,则掩码生成指令将提前执行,因此 xmm5 中的整数结果将有时间在 xmm0 准备好之前到达 ANDPS,即使它采用慢速通道。
Haswell 对整数结果没有旁路延迟 -> FP 布尔值,根据 Agner Fog 的测试。他对 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
- 融合域微操作:1 + 2*n(无内存操作)
- 添加到 dep 链的延迟:6c(Skylake:8c)
- 吞吐量:每 2c 1 个(两个端口 1 微指令)。 (Skylake:1/c,假设
MAXPS 使用与SUBPS 相同的两个端口。)
Skylake 丢弃了单独的向量-FP 添加单元,并在端口 0 和 1 上的 FMA 单元中进行向量添加。这使 FP 添加吞吐量翻倍,但延迟增加了 1c。 FMA latency is down to 4 (from 5 in *well)。 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
- 字节:9
- 融合域微操作:3*n(无内存操作)
- 添加到 dep 链的延迟:6c(Skylake:8c)
- 吞吐量:每 2c 1 个(两个端口 1 微指令)。 (Skylake:1/c)
- “延迟”如果
xmm0 在此序列发出时已准备好(无循环):相同
使用处理器识别的归零习惯(如xorps same,same)对寄存器进行归零是在 Sandbridge 系列微架构上的寄存器重命名期间处理的,并且具有零延迟和 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:使用整数
pcmpeq / shift 即时生成。不可能有缓存未命中,并且只会将 1c 添加到关键路径。
在一个不经常调用的函数中需要(在任何循环之外):只需优化大小(两个小版本都不使用内存中的常量)。非 AVX:3。 AVX:4。它们还不错,并且不能缓存未命中。关键路径的 4 个周期延迟比 1c 版本更糟糕,因此如果您认为 3 个指令字节不是什么大问题,请选择 1c。 4 版本适用于性能不重要且您希望避免溢出任何内容的注册压力情况。
AMD CPU:与ANDPS 之间存在旁路延迟(其本身具有 2c 延迟),但我认为它仍然是最佳选择。它仍然优于 SUBPS 的 5-6 个周期延迟。 MAXPS 是 2c 延迟。由于 Bulldozer 系列 CPU 上 FP 操作的高延迟,乱序执行更有可能及时生成掩码,以便在 @987654388 的其他操作数时准备好掩码@ 是。我猜推土机通过 Steamroller 没有单独的 FP 加法单元,而是在 FMA 单元中进行矢量加法和乘法运算。 3 在 AMD Bulldozer 系列 CPU 上始终是一个糟糕的选择。 2 在这种情况下看起来更好,因为从 fma 域到 fp 域再返回的旁路延迟更短。请参阅 Agner Fog 的微架构指南,第 182 页(15.11 不同执行域之间的数据延迟)。
Silvermont:与 SnB 相似的延迟。仍然使用 1c for 循环和概率。也可一次性使用。 Silvermont 出现故障,因此它可以提前准备好掩码,仍然只为关键路径增加 1 个周期。