tl; dr:我有两个功能相同的C代码,我用Clang编译(事实上它的C代码并不重要;只有汇编很有意思),IACA告诉我一个应该更快,但我不明白为什么,我的基准测试显示两个代码的性能相同.
我有以下的C代码(忽略#include "iacaMarks.h"
,IACA_START
,IACA_END
现在):
ref.c:
#include "iacaMarks.h" #include#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
):
resched.c:
#include "iacaMarks.h" #include#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 }
我正在使用Clang 5.0.0编译这些代码,目标是我的i5-6500(Skylake),带有标志-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条指令的调度,以及它们使用的寄存器.
我唯一可以想到的就是解释为什么第二个代码较慢的原因是它xmm0
在最后4个指令中写入两次,从而引入了一个依赖.但由于这些写操作是独立的,我希望CPU为它们使用不同的物理寄存器.但是,我无法真正证明这一理论.此外,如果使用两次xmm0
这样的问题,我希望Clang为其中一条指令使用不同的寄存器(特别是因为这里的寄存器压力很低).
我的问题:第二个代码应该更慢(基于汇编代码),为什么?
编辑:IACA跟踪:
第一版:https://pastebin.com/qGXHVW6a
第二版:https://pastebin.com/dbBNWsc2
注意:C代码是Serpent密码的第一个S-box的实现,由Osvik 在这里计算.
弄清楚为什么第二个代码是后端绑定需要一些手动分析,因为IACA发出的输出太粗糙,尽管信息非常丰富.请注意,IACA发出的迹线对于分析循环特别有用.它们对于理解指令的直线序列如何执行(这不是很有用)也很有用,但需要对发出的迹线进行不同的解释.通过这个答案的其余部分,我将展示我对循环场景的分析,这更难做到.
您在不将代码放入循环的情况下发出跟踪的事实会影响以下事项:
编译器无法内联并优化存储到输出操作数.它们不会出现在真正的循环中,或者将其链接到不同的S-box.
从输出到输入的数据依赖性是巧合发生的,因为编译器使用xmm0..3来准备要存储的数据,而不是选择将哪个输出反馈到同一个S-box的哪个输入.
在vpcmpeqd
创建一个全1向量(不)会内联后吊出循环.
将有一个dec/jnz
或等效的循环开销(可以宏端口融合到端口6的单个uop).
但是你已经要求IACA分析这个确切的asm块,好像它是在循环中运行一样.因此,为了解释结果,我们将如何看待它(即使它不是你从C编译器得到的,如果你在循环中使用这个函数).
在这种情况下,A jmp
或dec/jnz
底部使这个循环不是问题:它总是在端口6上执行,任何向量指令都不使用它.这意味着跳转指令不会在端口6上进行竞争,并且不会消耗本来由其他指令使用的调度程序uop带宽.但是,这会影响发布/重命名阶段的资源分配器带宽(每个周期不超过4个融合域uop),但这在我将讨论的特定情况下并不重要.
我们先来看一下端口压力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
融合域uop的总数为22.已经为端口0,1和5中的每一个分配了6个不同的uop.其他4个uop每个由STD和STA uop组成.STD需要端口4.此分配是合理的.如果我们忽略所有数据依赖性,那么调度程序应该能够在每个周期调度至少3个融合域uop.但是,在4号港口可能存在严重争用,这可能导致填满预订站.根据IACA,这不是此代码的瓶颈.请注意,如果调度程序可以某种方式实现等于分配器的最大吞吐量的吞吐量,则代码只能是前端绑定.显然,这不是这种情况.
下一步是仔细检查IACA跟踪.我根据跟踪制作了以下数据流图,更易于分析.水平黄线根据在同一周期中分配的uop来划分图形.请注意,IACA始终假设完美的分支预测.另请注意,此划分准确率约为99%,但不是100%.这并不重要,您可以认为它100%准确.节点表示融合的uops,箭头表示数据依赖性(箭头指向目标uop).节点根据它们属于哪个循环迭代而着色.为清楚起见,省略了图表顶部的箭头的来源.右侧的绿色框包含为相应的uops执行分配的循环编号.所以前一个周期是X,当前周期是X + 1,无论X是什么.停止标志表明相关的uop在其中一个端口遭受争用.所有红色停止标志代表端口1上的争用.只有另一个不同颜色的停止标志代表端口5上的争用.存在争用的情况,但为了清楚起见,我将省略它们.箭头有两种颜色:蓝色和红色.那些是关键的.注意,分配2次迭代的指令需要11个周期,然后重复分配模式.请记住,Skylake有97个RS.
每个部门("本地"位置)内的节点的位置具有含义.如果两个节点在同一行上,并且如果它们的所有操作数都可用,那么这意味着它们可以在同一个循环中调度.否则,如果节点不在同一行上,则可能不会在同一周期中调度它们.这仅适用于已作为一个组一起分配的动态uop,而不适用于作为不同组的一部分分配的动态uop,即使它们恰好位于图中的同一分区中.
我将使用符号(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 | | | | |
这告诉我们由于资源不可用(在这种情况下,保留站中的条目),此时分配带宽未得到充分利用.这意味着调度程序无法维持足够高的未融合uop吞吐量,以跟上每个周期的前端4个融合uop.既然IACA已经告诉我们代码是后端绑定的,那么显然这种未充分利用的原因不是因为某些长的依赖链或特定执行单元的争用,而是更复杂的东西.所以我们需要做更多的工作来弄清楚发生了什么.我们必须分析过去(11,5).
每次迭代的uops 1,4,7,10,13,18都分配给端口1.在11个周期内会发生什么?总共有12个uop需要端口1,所以不可能在11个周期内调度它们,因为它至少需要12个周期.不幸的是,uops中需要相同端口和需要其他端口的uop的数据依赖性会显着加剧问题.在11个周期内考虑以下管道流程:
在周期0:(0,0)和(0,1)被分配(以及我们现在不关心的其他uops).(0,1)与数据有关(0,0).
1:(0,4)和(0,7)被分配.假设没有为端口0分配较旧的就绪uop并且(0,0)的操作数已准备好,则将端口0调度(0,0).端口1可能保持空闲,因为(0,1)尚未就绪.
2:(0,0)的结果可通过旁路网络获得.此时,(0,1)可以并将被发送.但是,即使(0,4)或(0,7)准备就绪,也不是最早的uop分配给端口1,因此它们都被阻止.(0,10)被分配.
3:(0,4)被分派到端口1.(0,7)和(0,10)即使它们的操作数准备就被阻止.(0,13)被分配.
4:(0,7)被分派到端口1.(0,10)被阻塞.(0,13)必须等待(0,7).(0,18)被分配.
5:(0,10)被分派到端口1.(0,13)被阻塞.(0,18)必须等待(0,17),这取决于(0,13).(1,0)和(1,1)得到分配.
6:(0,13)被分派到端口1.(0,18)必须等待(0,17),这取决于(0,13).(1,1)必须等待(1,0).因为(1,0)和(0,7)之间的距离是3微秒,其中一个可能遭受端口冲突,所以不能调度(1,0).(1,4)被分配.
7:没有任何东西被分派到端口1,因为(0,18),(1,1)和(1,4)没有准备好.(1,7)得到分配.
8:没有任何东西被分派到端口1,因为(0,18),(1,1),(1,4)和(1,7)没有准备好.(1,10)和(1,13)被分配.
9:(0,18)被分派到端口1.(1,10)和(1,4)准备好但由于端口争用而被阻止.(1,1),(1,7)和(1,13)没有准备好.
10:(1,1)被分派到端口1.(1,4),(1,7)和(1,10)准备好但由于端口争用而被阻止.(1,13)尚未准备好.(1,18)被分配.
好吧,理想情况下,我们希望在11个周期中将12个uop中的11个发送到端口1.但是这种分析表明情况远非理想.端口1在11个周期中有4个空闲!如果我们假设前一次迭代中的某些(X,18)在周期0被调度,则端口1将空闲3个周期,这是很多浪费,考虑到我们有12个uop需要每11个周期.在12个uops中,最多只有8个被派遣.情况有多糟糕?我们可以继续分析跟踪并记录如何准备分派但由于冲突而阻塞的p1绑定的uop的数量,或者由于数据不足而未准备好的p1绑定的uop的数量.我能够确定由于端口冲突而停滞的p1绑定的uop的数量永远不会大于3.然而,由于数据的不足导致的p1绑定的uops数量随着时间的推移逐渐增加.我没有看到任何模式增加的方式,所以我决定在跟踪的前24个周期使用线性回归来预测在什么时候会有97个这样的uop.下图显示了这一点.
x轴表示从左到右增加的从零开始的循环.请注意,前4个周期的uop数为零.y轴表示相应周期的这种微量的数量.线性回归方程是:
y = 0.3624x - 0.6925.
通过将y设置为97,我们得到:
x =(97 + 0.6925)/0.3624 = 269.57
也就是说,在大约269周期,我们期望RS中有97个uop,所有p1-bound并等待它们的操作数准备就绪.此时RS已满.但是,由于其他原因,可能还有其他uops在RS中等待.因此,我们期望分配器在269周期或之前未充分利用其带宽.通过查看指令的IACA跟踪(11,5),我们可以看到情况发生在第61周期,这比269早得多.这意味着我的预测器非常乐观,或者与其他端口绑定的微量计数也表现出类似的行为.我的胆量告诉我它是后者.但这足以理解为什么IACA已经说过代码是后端绑定的.您可以对第一个代码执行类似的分析,以了解它为什么是前端绑定.我想我只是留给读者练习.
如果IACA不支持特定的代码或者特定的微结构不存在像IACA这样的工具,则可以遵循此手动分析.线性回归模型能够在分配器未充分利用其带宽的迭代次数之后进行估计.例如,在这种情况下,周期269对应于迭代269/11/2 = 269/22 = 12.因此,只要最大迭代次数不大于12,循环的后端性能就会小于问题.
@Bee有一个相关的帖子:x86 uops是如何安排的,确切地说?.
我可以发布前24个周期内发生的细节.
附注:Wikichip关于Skylake的文章中有两个错误.首先,Broadwell的调度程序有60个,而不是64个.其次,分配器的吞吐量最多只有4个融合的uop.