tl; dr :我有两个用Clang编译的功能等效的C代码(事实是C代码没什么大不了;我认为只有程序集很有趣)和IACA告诉我一个应该更快,但是我不明白为什么,我的基准测试显示这两个代码具有相同的性能。
我有以下C代码(暂时忽略#include "iacaMarks.h"
,IACA_START
,IACA_END
)
ref.c:
#include "iacaMarks.h"
#include <x86intrin.h>
#define AND(a,b) _mm_and_si128(a,b)
#define OR(a,b) _mm_or_si128(a,b)
#define XOR(a,b) _mm_xor_si128(a,b)
#define NOT(a) _mm_andnot_si128(a,_mm_set1_epi32(-1))
void sbox_ref (__m128i r0,__m128i r1,__m128i r2,__m128i r3,
__m128i* r5,__m128i* r6,__m128i* r7,__m128i* r8) {
__m128i r4;
IACA_START
r3 = XOR(r3,r0);
r4 = r1;
r1 = AND(r1,r3);
r4 = XOR(r4,r2);
r1 = XOR(r1,r0);
r0 = OR(r0,r3);
r0 = XOR(r0,r4);
r4 = XOR(r4,r3);
r3 = XOR(r3,r2);
r2 = OR(r2,r1);
r2 = XOR(r2,r4);
r4 = NOT(r4);
r4 = OR(r4,r1);
r1 = XOR(r1,r3);
r1 = XOR(r1,r4);
r3 = OR(r3,r0);
r1 = XOR(r1,r3);
r4 = XOR(r4,r3);
*r5 = r1;
*r6 = r4;
*r7 = r2;
*r8 = r0;
IACA_END
}
我想知道是否可以通过手动重新调度一些指令来优化它(我很清楚C编译器应该产生有效的调度,但是我的实验表明情况并非总是如此)。在某些时候,我尝试了以下代码(与上面的代码相同,不同之处在于,没有临时变量用于存储稍后分配给*r5
和*r6
的XOR的结果):>
resched.c:
#include "iacaMarks.h"
#include <x86intrin.h>
#define AND(a,b) _mm_and_si128(a,b)
#define OR(a,b) _mm_or_si128(a,b)
#define XOR(a,b) _mm_xor_si128(a,b)
#define NOT(a) _mm_andnot_si128(a,_mm_set1_epi32(-1))
void sbox_resched (__m128i r0,__m128i r1,__m128i r2,__m128i r3,
__m128i* r5,__m128i* r6,__m128i* r7,__m128i* r8) {
__m128i r4;
IACA_START
r3 = XOR(r3,r0);
r4 = r1;
r1 = AND(r1,r3);
r4 = XOR(r4,r2);
r1 = XOR(r1,r0);
r0 = OR(r0,r3);
r0 = XOR(r0,r4);
r4 = XOR(r4,r3);
r3 = XOR(r3,r2);
r2 = OR(r2,r1);
r2 = XOR(r2,r4);
r4 = NOT(r4);
r4 = OR(r4,r1);
r1 = XOR(r1,r3);
r1 = XOR(r1,r4);
r3 = OR(r3,r0);
*r7 = r2;
*r8 = r0;
*r5 = XOR(r1,r3); // This two lines are different
*r6 = XOR(r4,r3); // (no more temporary variables)
IACA_END
}
我正在使用针对我的i5-6500(Skylake)的Clang 5.0.0编译这些代码,并带有标志-O3 -march=native
(我省略了生成的汇编代码,因为它们可以在IACA输出中找到在下面,但是如果您希望直接将它们放在这里,请问我,我会添加它们。我对这两个代码进行了基准测试,发现它们之间没有任何性能差异。出于好奇,我对它们运行了IACA,我惊讶地发现它说第一个版本需要6个周期才能运行,而第二个版本需要7个周期。
这是IACA产生的输出:
对于第一个版本:
dada@dada-ubuntu ~/perf % clang -O3 -march=native -c ref.c && ./iaca -arch SKL ref.o
Intel(R) Architecture Code Analyzer Version - v3.0-28-g1ba2cbb build date: 2017-10-23;16:42:45
Analyzed File - ref_iaca.o
Binary Format - 64Bit
Architecture - SKL
Analysis Type - Throughput
Throughput Analysis Report
--------------------------
Block Throughput: 6.00 Cycles Throughput Bottleneck: FrontEnd
Loop Count: 23
Port Binding In Cycles Per Iteration:
--------------------------------------------------------------------------------------------------
| Port | 0 - DV | 1 | 2 - D | 3 - D | 4 | 5 | 6 | 7 |
--------------------------------------------------------------------------------------------------
| Cycles | 6.0 0.0 | 6.0 | 1.3 0.0 | 1.4 0.0 | 4.0 | 6.0 | 0.0 | 1.4 |
--------------------------------------------------------------------------------------------------
DV - Divider pipe (on port 0)
D - Data fetch pipe (on ports 2 and 3)
F - Macro Fusion with the previous instruction occurred
* - instruction micro-ops not bound to a port
^ - Micro Fusion occurred
# - ESP Tracking sync uop was issued
@ - SSE instruction followed an AVX256/AVX512 instruction, dozens of cycles penalty is expected
X - instruction not supported, was not accounted in Analysis
| Num Of | Ports pressure in cycles | |
| Uops | 0 - DV | 1 | 2 - D | 3 - D | 4 | 5 | 6 | 7 |
-----------------------------------------------------------------------------------------
| 1 | 1.0 | | | | | | | | vpxor xmm4, xmm3, xmm0
| 1 | | 1.0 | | | | | | | vpand xmm5, xmm4, xmm1
| 1 | | | | | | 1.0 | | | vpxor xmm1, xmm2, xmm1
| 1 | 1.0 | | | | | | | | vpxor xmm5, xmm5, xmm0
| 1 | | 1.0 | | | | | | | vpor xmm0, xmm3, xmm0
| 1 | | | | | | 1.0 | | | vpxor xmm0, xmm0, xmm1
| 1 | 1.0 | | | | | | | | vpxor xmm1, xmm4, xmm1
| 1 | | 1.0 | | | | | | | vpxor xmm3, xmm4, xmm2
| 1 | | | | | | 1.0 | | | vpor xmm2, xmm5, xmm2
| 1 | 1.0 | | | | | | | | vpxor xmm2, xmm2, xmm1
| 1 | | 1.0 | | | | | | | vpcmpeqd xmm4, xmm4, xmm4
| 1 | | | | | | 1.0 | | | vpxor xmm1, xmm1, xmm4
| 1 | 1.0 | | | | | | | | vpor xmm1, xmm5, xmm1
| 1 | | 1.0 | | | | | | | vpxor xmm4, xmm5, xmm3
| 1 | | | | | | 1.0 | | | vpor xmm3, xmm0, xmm3
| 1 | 1.0 | | | | | | | | vpxor xmm4, xmm4, xmm3
| 1 | | 1.0 | | | | | | | vpxor xmm4, xmm4, xmm1
| 1 | | | | | | 1.0 | | | vpxor xmm1, xmm1, xmm3
| 2^ | | | 0.3 | 0.3 | 1.0 | | | 0.3 | vmovdqa xmmword ptr [rdi], xmm4
| 2^ | | | 0.3 | 0.3 | 1.0 | | | 0.3 | vmovdqa xmmword ptr [rsi], xmm1
| 2^ | | | 0.3 | 0.3 | 1.0 | | | 0.3 | vmovdqa xmmword ptr [rdx], xmm2
| 2^ | | | 0.3 | 0.3 | 1.0 | | | 0.3 | vmovdqa xmmword ptr [rcx], xmm0
Total Num Of Uops: 26
对于第二个版本:
dada@dada-ubuntu ~/perf % clang -O3 -march=native -c resched.c && ./iaca -arch SKL resched.o
Intel(R) Architecture Code Analyzer Version - v3.0-28-g1ba2cbb build date: 2017-10-23;16:42:45
Analyzed File - resched_iaca.o
Binary Format - 64Bit
Architecture - SKL
Analysis Type - Throughput
Throughput Analysis Report
--------------------------
Block Throughput: 7.00 Cycles Throughput Bottleneck: Backend
Loop Count: 22
Port Binding In Cycles Per Iteration:
--------------------------------------------------------------------------------------------------
| Port | 0 - DV | 1 | 2 - D | 3 - D | 4 | 5 | 6 | 7 |
--------------------------------------------------------------------------------------------------
| Cycles | 6.0 0.0 | 6.0 | 1.3 0.0 | 1.4 0.0 | 4.0 | 6.0 | 0.0 | 1.3 |
--------------------------------------------------------------------------------------------------
DV - Divider pipe (on port 0)
D - Data fetch pipe (on ports 2 and 3)
F - Macro Fusion with the previous instruction occurred
* - instruction micro-ops not bound to a port
^ - Micro Fusion occurred
# - ESP Tracking sync uop was issued
@ - SSE instruction followed an AVX256/AVX512 instruction, dozens of cycles penalty is expected
X - instruction not supported, was not accounted in Analysis
| Num Of | Ports pressure in cycles | |
| Uops | 0 - DV | 1 | 2 - D | 3 - D | 4 | 5 | 6 | 7 |
-----------------------------------------------------------------------------------------
| 1 | 1.0 | | | | | | | | vpxor xmm4, xmm3, xmm0
| 1 | | 1.0 | | | | | | | vpand xmm5, xmm4, xmm1
| 1 | | | | | | 1.0 | | | vpxor xmm1, xmm2, xmm1
| 1 | 1.0 | | | | | | | | vpxor xmm5, xmm5, xmm0
| 1 | | 1.0 | | | | | | | vpor xmm0, xmm3, xmm0
| 1 | | | | | | 1.0 | | | vpxor xmm0, xmm0, xmm1
| 1 | 1.0 | | | | | | | | vpxor xmm1, xmm4, xmm1
| 1 | | 1.0 | | | | | | | vpxor xmm3, xmm4, xmm2
| 1 | | | | | | 1.0 | | | vpor xmm2, xmm5, xmm2
| 1 | 1.0 | | | | | | | | vpxor xmm2, xmm2, xmm1
| 1 | | 1.0 | | | | | | | vpcmpeqd xmm4, xmm4, xmm4
| 1 | | | | | | 1.0 | | | vpxor xmm1, xmm1, xmm4
| 1 | 1.0 | | | | | | | | vpor xmm1, xmm5, xmm1
| 1 | | 1.0 | | | | | | | vpxor xmm4, xmm5, xmm3
| 1 | | | | | | 1.0 | | | vpor xmm3, xmm0, xmm3
| 2^ | | | 0.3 | 0.4 | 1.0 | | | 0.3 | vmovdqa xmmword ptr [rdx], xmm2
| 2^ | | | 0.3 | 0.3 | 1.0 | | | 0.4 | vmovdqa xmmword ptr [rcx], xmm0
| 1 | 1.0 | | | | | | | | vpxor xmm0, xmm4, xmm3
| 1 | | 1.0 | | | | | | | vpxor xmm0, xmm0, xmm1
| 2^ | | | 0.4 | 0.3 | 1.0 | | | 0.3 | vmovdqa xmmword ptr [rdi], xmm0
| 1 | | | | | | 1.0 | | | vpxor xmm0, xmm1, xmm3
| 2^ | | | 0.3 | 0.4 | 1.0 | | | 0.3 | vmovdqa xmmword ptr [rsi], xmm0
Total Num Of Uops: 26
Analysis Notes:
Backend allocation was stalled due to unavailable allocation resources.
如您所见,在第二版中,IACA表示瓶颈是后端,并且“后端分配由于分配资源不可用而停滞了。”
两个汇编代码都包含相同的指令,唯一的区别是最后7条指令的调度以及它们使用的寄存器。
我唯一想到的就是可以解释第二个代码为什么要慢一些的事实,因为它在最后4条指令中写了两次xmm0
,从而引入了依赖性。但是由于这些写操作是独立的,所以我希望CPU为它们使用不同的物理寄存器。但是,我无法真正证明这一理论。另外,如果像这样使用两次xmm0
是个问题,我希望Clang对其中一条指令使用不同的寄存器(特别是因为这里的寄存器压力很低)。
我的问题:第二个代码应该慢一些吗(基于汇编代码),为什么?
编辑:IACA跟踪:
第一版:https://pastebin.com/qGXHVW6a
第二版:https://pastebin.com/dbBNWsc2
注意:C代码是由Osvik here计算得出的蛇密码第一个S-box的实现。
答案 0 :(得分:5)
弄清楚为什么第二个代码是后端绑定的,需要进行一些手动分析,因为IACA发出的输出太原始了,尽管信息非常丰富。请注意,IACA发出的迹线对于分析循环特别有用,它们对于理解如何执行直线指令序列(这没有用)也很有用,但是发出的迹线需要进行不同的解释。贯穿此答案的其余部分,我将介绍对循环场景的分析,这比较困难。
在不将代码放入循环的情况下发出跟踪的事实会影响以下内容:
vpcmpeqd
悬挂在循环之外。dec/jnz
或同等的循环开销(可以将其宏融合为端口6的单个uop)。但是您已经要求IACA分析这个asm精确块,就好像它是在循环中运行一样。因此,要解释结果,这就是我们如何看待它(即使您不是在循环中使用此函数,也不会从C编译器得到它)。
在这种情况下,底部使jmp
或dec/jnz
成为循环不是问题:它将始终在端口6上执行,任何向量指令都不会使用它。这意味着跳转指令将不会在端口6上竞争,也不会消耗其他指令可能已使用的调度程序uop带宽。但是,这可能会在问题/重命名阶段(每个周期不超过4个融合域uops)影响资源分配器的带宽,但这在这种特殊情况下并不重要,正如我将要讨论的。
我们首先检查端口压力ASCII数字:
| Num Of | Ports pressure in cycles | |
| Uops | 0 - DV | 1 | 2 - D | 3 - D | 4 | 5 | 6 | 7 |
-----------------------------------------------------------------------------------------
| 1 | 1.0 | | | | | | | | vpxor xmm4, xmm3, xmm0
| 1 | | 1.0 | | | | | | | vpand xmm5, xmm4, xmm1
| 1 | | | | | | 1.0 | | | vpxor xmm1, xmm2, xmm1
| 1 | 1.0 | | | | | | | | vpxor xmm5, xmm5, xmm0
| 1 | | 1.0 | | | | | | | vpor xmm0, xmm3, xmm0
| 1 | | | | | | 1.0 | | | vpxor xmm0, xmm0, xmm1
| 1 | 1.0 | | | | | | | | vpxor xmm1, xmm4, xmm1
| 1 | | 1.0 | | | | | | | vpxor xmm3, xmm4, xmm2
| 1 | | | | | | 1.0 | | | vpor xmm2, xmm5, xmm2
| 1 | 1.0 | | | | | | | | vpxor xmm2, xmm2, xmm1
| 1 | | 1.0 | | | | | | | vpcmpeqd xmm4, xmm4, xmm4
| 1 | | | | | | 1.0 | | | vpxor xmm1, xmm1, xmm4
| 1 | 1.0 | | | | | | | | vpor xmm1, xmm5, xmm1
| 1 | | 1.0 | | | | | | | vpxor xmm4, xmm5, xmm3
| 1 | | | | | | 1.0 | | | vpor xmm3, xmm0, xmm3
| 2^ | | | 0.3 | 0.4 | 1.0 | | | 0.3 | vmovdqa xmmword ptr [rdx], xmm2
| 2^ | | | 0.3 | 0.3 | 1.0 | | | 0.4 | vmovdqa xmmword ptr [rcx], xmm0
| 1 | 1.0 | | | | | | | | vpxor xmm0, xmm4, xmm3
| 1 | | 1.0 | | | | | | | vpxor xmm0, xmm0, xmm1
| 2^ | | | 0.4 | 0.3 | 1.0 | | | 0.3 | vmovdqa xmmword ptr [rdi], xmm0
| 1 | | | | | | 1.0 | | | vpxor xmm0, xmm1, xmm3
| 2^ | | | 0.3 | 0.4 | 1.0 | | | 0.3 | vmovdqa xmmword ptr [rsi], xmm0
融合域uops的总数为22。已经为端口0、1和5分配了6个不同的uops。其他4个uops分别由STD和STA uops组成。 STD需要端口4。此分配是合理的。如果我们忽略所有数据相关性,则看来调度程序应该能够在每个周期内至少分配3个融合域uops。但是,端口4可能存在严重争用,这可能导致填满预订站。根据IACA,这不是此代码中的瓶颈。注意,如果调度程序可以某种方式实现等于分配器最大吞吐量的吞吐量,则代码只能是前端绑定的。显然,这里不是这样。
下一步是仔细检查IACA跟踪。我根据跟踪制作了以下数据流图,该图更易于分析。水平的黄线根据在同一周期内分配的微指令来划分图形。请注意,IACA始终假设完善的分支预测。另请注意,该划分的准确度约为99%,但并非100%。这并不重要,您可以认为它100%准确。节点表示融合的uops,箭头表示数据依赖性(箭头指向目标uop)。节点的颜色取决于它们所属的循环迭代。为了清楚起见,省略了图形顶部箭头的来源。右侧的绿色框包含循环编号,在该循环编号处为相应的微指令进行分配。因此,前一个周期是X,而当前周期是X + 1,无论X是多少。停止标志表明相关联的uop在其中一个端口上发生争用。所有红色停止符号表示在端口1上的争用。只有另外一个不同颜色的停止符号表示在端口5上的争用。存在争用的情况,为清楚起见,我将省略它们。箭头有两种颜色:蓝色和红色。那些是关键的。请注意,分配2个迭代值的指令需要11个周期,然后重复分配模式。请记住,Skylake具有97个RS整体。
每个分区内节点的位置(“本地”位置)具有含义。如果两个节点在同一行上,并且它们的所有操作数均可用,则意味着可以在同一周期内分派它们。否则,如果节点不在同一行上,则它们可能不会在同一周期内分派。这仅适用于已一起分配为一组的动态uops,而不适用于作为不同组的一部分分配的动态uops,即使它们恰好位于图中的同一划分中。
我将使用符号(it, in)
来标识特定的融合uop,其中it
是从零开始的循环迭代数,而in
是从零开始的uop数。 IACA跟踪中最重要的部分是显示(11,5)的流水线阶段的部分:
11| 5|vpxor xmm0, xmm0, xmm1 : | | | | | | | | | | | | | |
11| 5| TYPE_OP (1 uops) : | | | | | |_A--------------------dw----R-------p | | | | |
这告诉我们,由于资源不可用(在这种情况下,是保留站中的条目),分配带宽在此时未得到充分利用。这意味着调度程序无法维持足够高的未融合微指令吞吐量,无法跟上每个周期前端4个融合微指令的速度。由于IACA已经告诉我们代码是后端绑定的,所以很明显,这种未充分利用的原因不是由于特定执行单元上的较长依赖链或争用,而是更复杂的原因。因此,我们需要做更多的工作来弄清楚发生了什么。我们必须分析过去(11,5)。
每次迭代的uops 1、4、7、10、13、18都分配给端口1。在11个周期内会发生什么?总共需要12个微指令,需要端口1,因此不可能在11个周期内分派所有微指令,因为这至少需要12个周期。不幸的是,需要相同端口的uops内部和需要其他端口的uops之间的数据依赖性使问题变得更加严重。在一个11个周期内,请考虑以下管道流量:
好吧,理想情况下,我们希望在12个微指令中有11个以11个周期分配到端口1。但是,这种分析表明,情况远非理想。端口1在11个周期中有4个空闲!如果我们假设前一个迭代中的某些(X,18)在周期0处调度,则端口1将空闲3个周期,这是很多浪费,考虑到我们每11个周期需要12个微指令。在12个微指令中,只有8个被调度。情况有多严重?我们可以继续分析跟踪并记录已准备好分发但由于冲突而被阻止或由于数据下降而未准备就绪的p1绑定的数量。我能够确定由于端口冲突而停滞的p1绑定的数量永远不会大于3。但是,由于数据下降而停滞的p1绑定的数量总体上会随着时间逐渐增加。我没有看到其增加方式的任何模式,因此我决定在跟踪的前24个周期中使用线性回归来预测在什么时候会有97个这样的微指令。下图显示了这一点。
x轴表示从左到右递增的从零开始的循环。请注意,在前4个周期内,微指令的数量为零。 y轴表示相应循环中此类微指令的数量。线性回归方程为:
y = 0.3624x-0.6925。
通过将y设置为97,我们得到:
x =(97 + 0.6925)/ 0.3624 = 269.57
也就是说,在大约第269个周期,我们期望RS中所有p1绑定到97微码,并等待其操作数准备就绪。此时RS已满。但是,由于其他原因,RS中可能还有其他等待的对象。因此,我们期望分配器在周期269或之前未充分利用其带宽。通过查看指令(11,5)的IACA跟踪,我们可以看到情况发生在周期629,这早于269。这意味着我的预测器非常乐观,或者与其他端口绑定的uops数量也表现出类似的行为。我的胆量告诉我是后者。但这足以理解IACA为什么说代码是后端绑定的。您可以对第一个代码执行类似的分析,以了解为什么它是前端绑定的。我想我只是留给读者练习。
在IACA不支持特定代码段或不存在针对特定微体系结构的IACA之类的工具的情况下,可以执行此手动分析。线性回归模型可以估计在分配了多少次迭代后分配器未充分利用其带宽。例如,在这种情况下,周期269对应于迭代269/11/2 = 269/22 =12。因此,只要最大迭代次数不大于12,循环的后端性能就会降低问题。
@Bee有一篇相关的文章:How are x86 uops scheduled, exactly?。
我可能会在之后的前24个周期中发布详细信息。
旁注:Wikichip在Skylake上的文章有两个错误。首先,Broadwell的调度程序具有60个整体,而不是64个。其次,分配器的吞吐量最多只有4个融合uops。
答案 1 :(得分:2)
我对这两个代码进行了基准测试,发现两者之间没有任何性能差异。
我在Skylake i7-6700k上做了同样的事情,实际上是基准测试,告诉你IACA分析的内容,方法是收集该asm并在其周围拍一个dec ebp / jnz .loop
。
我发现sbox_ref
的运行迭代约为7.50个周期,而sbox_resched
的运行约为〜8.04 c / iter ,并在Linux上的静态可执行文件中通过性能计数器进行了测试。 (有关我的测试方法的详细信息,请参见Can x86's MOV really be "free"? Why can't I reproduce this at all?)。
IACA的数字有误,但sbox_resched
的速度较慢是正确的。
Hadi的分析似乎是正确的:asm中的依赖项链足够长,以至于uop调度中的任何资源冲突都将导致后端失去吞吐量,而后者永远无法追赶。
大概是通过让C编译器将函数内联到循环中来进行测试,并为输出操作数提供局部变量。 (这与我在编写自己的@Hadi答案之前编辑的项目符号要点相反):
在编译器使用xmm0..3作为函数后期的暂存器时,不是偶然发生的,而是从输出到输入的数据依存关系对于编译器是可见的,因此可以适当地进行调度。您的源代码将选择将哪个输出反馈到同一S-box的哪个输入。
或者deps不存在(如果您使用常量输入,并避免使用volatile
或空的内联asm语句使循环最优化)。
输出操作数的存储空间被优化了,就像将其链接到另一个S-box上那样。
vpcmpeqd
悬挂在循环之外。正如Hadi所说,1 uop宏融合的dec/jnz
循环开销不会争夺矢量ALU,因此它本身并不重要。至关重要的是,将asm循环拍在编译器未优化的内容周围,因为循环体毫不奇怪地给出了愚蠢的结果。