简而言之:几乎所有情况下,使用pcmeq / shift生成掩码,并使用andps使用它。 它的关键路径远远最短(与来自内存的常量并列),且不会出现缓存未命中。
如何使用内部函数实现
使编译器在未初始化的寄存器上发出pcmpeqd
可能有些棘手。(godbolt)。gcc / icc 的最佳方式似乎是
__m128 abs_mask(void){
__m128i minus1 = _mm_set1_epi32(-1);
return _mm_castsi128_ps(_mm_srli_epi32(minus1, 1));
}
__m128 vecabs_and(__m128 v) {
return _mm_and_ps(abs_mask(), v);
}
__m128 sumabs(const __m128 *a) {
__m128 sum = vecabs_and(*a);
for (int i=1 ; i < 10000 ; i++) {
sum = _mm_add_ps(sum, vecabs_and(a[i]));
}
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。
待办事项:找出如何避免在内联中到处复制常量。可能使用全局常量而不是匿名 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寄存器,movd,shufps),而movd在AMD上很慢,因为向量单元在两个整数核之间共享。(它们的超线程版本。)
选择最佳汇编序列
好的,让我们以英特尔Sandybridge到Skylake为例进行研究,还要稍微提及Nehalem。请参阅Agner Fog的 微架构指南和指令计时,了解我是如何得出这个结论的。我还使用了在http://realwordtech.com/ 论坛帖子中发布的Skylake数据。
假设我们想要对向量进行
abs()
,它在
xmm0
中,并且像 FP 代码一样是长依赖链的一部分。
因此,假设任何不依赖于
xmm0
的操作可以在几个周期之前开始。我已经测试过,具有内存操作数的指令不会为依赖链增加额外的延迟,假设内存操作数的地址不是依赖链的一部分(即不是关键路径的一部分)。
我不完全清楚当内存操作作为微融合uop的一部分时可以提前多少开始。据我了解,重排序缓冲区(ROB)与融合uops配合使用,并跟踪从发布到退役的uops(168(SnB)到224(SKL)个条目)。还有一个调度程序在未融合领域中工作,仅保留已准备好其输入操作数但尚未执行的uops。当它们被解码(或从uop高速缓存加载)时,uops可以同时进入ROB(融合)和调度程序(未融合)。
如果我理解正确,在Sandybridge到Broadwell中为54到64个条目, 在Skylake中为97个。
有一些毫无根据的猜测认为它不再是统一的(ALU / load-store)调度程序。
还有关于Skylake每个时钟周期处理6个uops的说法。据我所知,Skylake将整个uop缓存行(最多6个uops)每个时钟周期读入到uop缓存和ROB之间的缓冲区中。ROB/调度器的发行仍然是4宽度的(即使是nop也是每个时钟周期4个)。这个缓冲区有助于解决
代码对齐/uop缓存行边界导致之前Sandybridge微架构设计瓶颈的问题。我之前认为这个“发行队列”就是这个缓冲区,但显然不是这样。
无论如何,如果地址不在关键路径上,调度器足够大,可以及时从缓存中获取数据。
1a:使用内存操作数进行掩码
ANDPS xmm0, [mask]
- 字节数:7个指令,16个数据。(AVX:8个指令)
- 融合域微操作:1 * n
- 关键路径加入的延迟:1c(假设L1缓存命中)
- 吞吐量:1/c。(Skylake: 2/c)(受限于每个周期2次加载)
- 如果
xmm0
在此指令发出时准备就绪,则为“延迟”:在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指令 + 16数据。(AVX:12指令字节)
- 融合域uops: 1 + 1*n
- 延迟添加到依赖链中: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)
- 融合域uops:2 + 1*n (无内存操作)
- 添加到依赖链的延迟时间:1c
- 吞吐量:1/c。 (Skylake: 3/c)
- 所有3个uop的吞吐量:1/c,饱和了3个向量ALU端口
- "延迟"如果
xmm0
在此序列发出时已准备就绪(没有循环):3c(+1c可能的旁路延迟,如果ANDPS必须等待整数数据就绪。 Agner Fog表示,在某些情况下,在SnB / IvB上进行整数-> FP-boolean不会有额外的延迟。)
这个版本仍然比内存中带有16B常量的版本占用更少的内存。 对于不经常调用的函数也非常理想,因为没有负载需要遭受缓存未命中。
"绕过延迟"不应该成为问题。如果xmm0是长依赖链的一部分,生成掩码指令将会提前执行,因此xmm5中的整数结果将有足够的时间在xmm0准备好之前到达ANDPS,即使它采用了慢车道。
根据Agner Fog的测试,Haswell没有整数结果到FP布尔值的旁路延迟。他对SnB/IvB的描述表明,这是某些整数指令的输出情况。因此,即使在“起步”依赖链开头的情况下,当这个指令序列发出时,如果
xmm0
已经准备好了,那么在*well上只需要3个时钟周期,在*Bridge上需要4个时钟周期。如果执行单元正在以被发出的速度清除uops的积压,则延迟可能并不重要。
无论如何,ANDPS的输出将在FP域中,并且如果用于
MULPS
等操作,则不会有旁路延迟。
在 Nehalem 上,绕过延迟为 2c。因此,在 Nehalem 上的 dep 链的开头(例如,在分支错误预测或 I$ 错误之后),如果
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 (没有内存操作)
- 添加到依赖链的延迟: 6c (Skylake: 8c)
- 吞吐量: 每2个周期1次(两个端口1 uops)。(Skylake: 1/c,假设
MAXPS
使用与SUBPS
相同的两个端口。)
Skylake取消了单独的向量FP加法单元,并在端口0和1上的FMA单元中执行向量加法。这将加倍FP加法吞吐量,但代价是多1个周期的延迟。FMA延迟降至4(从*well的5)。x87 FADD
仍然是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
- 融合域 uops:3*n(无内存操作)
- 延迟添加到依赖链中:6c(Skylake:8c)
- 吞吐量:每2个周期1个(两个端口1 uop)。 (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
- 融合域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
在此序列发出时已准备就绪(无循环):相同
这应该可以工作,但我不知道NaN会发生什么。很好的观察是ANDPS具有较低的延迟,并且不需要FPU加法端口。
这是非-AVX的最小尺寸。
4: 左/右移位:
PSLLD xmm0, 1
PSRLD xmm0, 1
我假设从FP到整数移位有1c的旁路延迟,然后又有1c的延迟返回,因此这与SUBPS / ANDPS一样慢。 它确实节省了一个无执行端口uop,因此如果融合域uop吞吐量是问题,并且您无法将掩码生成移出循环,则具有优势。 (例如,因为这是在循环中调用的函数,而不是内联的)。
何时使用何种方法:从内存中加载掩码使代码简单,但有缓存未命中的风险。而且需要占用16B的只读数据,而不是9个指令字节。
在循环中需要:1c:在循环外生成掩码(使用pcmp/shift);在内部使用单个 andps
。如果你不能节约寄存器,请将其溢出到堆栈中,并使用1a:andps xmm0,[rsp + mask_local]
。(生成和存储不太可能导致缓存未命中,而常量则会)。无论如何都只增加了1个单-uop指令的关键路径的一个周期。这是一个端口5的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更快))
在频繁调用的非循环函数中(因此您不能将掩码生成分摊到多个用途中):
- 如果uop吞吐量是一个问题:1a:
andps xmm0,[mask]
。偶尔的缓存未命中应该分摊了uops的节省,如果那真的是瓶颈的话。
- 如果延迟不是问题(函数仅用作短非循环传递的依赖链的一部分,例如
arr [i] = abs(2.0 + arr [i]);
),并且您想要避免内存中的常量:4,因为它只有2个uop。如果abs
在依赖链的开头或结尾,则不会从负载到存储产生旁路延迟。
- 如果uop吞吐量不是问题: 1c :使用整数
pcmpeq / shift
即时生成。不可能缓存未命中,并且关键路径仅增加了1c。
在稀少调用的函数中需要(在任何循环之外):只需针对大小进行优化(两个较小的版本都不使用来自内存的常量)。非AVX:3。AVX:4。它们并不差,并且不能缓存未命中。与版本1c获得的4个周期延迟相比,关键路径的4个周期延迟要更糟糕,因此如果您认为3个指令字节不是很重要,则选择 1c 。版本 4 适用于寄存器压力情况下,性能不重要,并且您希望避免溢出任何内容。
AMD CPU:在 ANDPS
(本身具有2c的延迟) 的通路中有一个绕过延迟,但我认为它仍然是最佳选择。它仍然能够击败 SUBPS
的5-6个周期延迟。 MAXPS
具有2c的延迟。由于Bulldozer系列CPU上FP操作的高延迟,你更有可能通过乱序执行,在另一个操作数到达 ANDPS
时即时生成屏蔽位。我猜测 Bulldozer 到 Steamroller 没有单独的FP加法器,而是在FMA单元中进行向量加法和乘法。3 总是在AMD Bulldozer系列CPU上很差。在这种情况下,2 看起来更好,因为从fma域到fp域和返回的旁路延迟更短。请参阅Agner Fog的微架构指南,第182页(15.11不同执行域之间的数据延迟)。
Silvermont:与SnB类似的延迟。仍然使用 1c 进行循环,对于一次性使用也很适合。Silvermont是乱序执行的,因此它可以提前准备好屏蔽位,仍然只增加1个周期到关键路径。
andps
与之配合,对于大多数情况来说是最佳解决方案。 - Peter Cordes