我们在Fortran中有一个科学代码,我们正在使用VTune进行一些热点分析。它识别的最大热点是END DO
。深入到汇编代码中,END DO
上花费的时间的1/4在jle
命令中。剩余的3/4位于循环索引变量的incq
命令中。
END DO
行分为addq
,incq
,cmpq
和jle
,我希望比较运算符的排长时间最长。
但令我感到困惑的是为什么incq
花了这么长时间。在这种特殊情况下,循环执行12次,仅在incq
上花费120毫秒的总时间。为什么增加索引变量需要这么长时间?
这有点乱,但这里是原始循环,其中初始数字是与汇编代码对应的行号:
796 DO NS = 1, NSPECI
857 IF (Tguess(iter) < Tmid(ns)) THEN
858 M = 1
859 ELSE
860 M = 2
861 END IF
862
863 Cpp = thcoef(1,m,ns)
864 Hfp = thcoef(1,m,ns) * Tguess(iter)
865
866 a_nTn = thcoef(2,m,ns) * Tguess(iter)
867 Cpp = Cpp + a_nTn
868 Hfp = Hfp + R2I * a_nTn * Tguess(iter)
869
870 a_nTn = thcoef(3,m,ns) * T2
871 Cpp = Cpp + a_nTn
872 Hfp = Hfp + R3I * a_nTn * Tguess(iter)
873
874 a_nTn = thcoef(4,m,ns) * T3
875 Cpp = Cpp + a_nTn
876 Hfp = Hfp + R4I * a_nTn * Tguess(iter)
877
878 a_nTn = thcoef(5,m,ns) * T4
879 Cpp = Cpp + a_nTn
880 Hfp = Hfp + R5I * a_nTn * Tguess(iter) + thcoef(6,m,ns)
881
883 CVP = CPP - 1.0_wp
884 hf = hf + hfp * Rgk_Yk(ns)
885 Cv = Cv + Cvp * Rgk_Yk(ns)
886 END DO
和生成的程序集:
movq 112(%rax), %rdx #863.22
movq 104(%rax), %r15 #863.22
imulq %r15, %rdx #
movq 88(%rax), %rsi #863.22
movq 80(%rax), %rcx #863.22
imulq %rcx, %rsi #
movq %r8, -176(%rbp) #
movq thermo_m_mp_thcoef_(%rip), %r8 #863.16
movq 64+thermo_m_mp_tmid_(%rip), %r14 #857.20
subq %rdx, %r8 #
shlq $3, %r14 #
subq %rsi, %r8 #
movq %rdi, -392(%rbp) #
negq %r14 #
movq 64+thermo_m_mp_thcoef_(%rip), %rdi #863.16
shlq $3, %rdi #
subq %rdi, %r8 #
movq %rcx, -248(%rbp) #863.22
movq %r8, -240(%rbp) #
movsd .L_2il0floatpacket.46(%rip), %xmm6 #868.32
movsd .L_2il0floatpacket.47(%rip), %xmm5 #872.32
movsd .L_2il0floatpacket.48(%rip), %xmm4 #876.32
movsd .L_2il0floatpacket.49(%rip), %xmm3 #880.32
pushq $1 #796.13
popq %rcx #796.13
movsd -280(%rbp), %xmm8 #
movq %r15, %rdx #
movsd -208(%rbp), %xmm2 #
movsd -264(%rbp), %xmm1 #
movsd .L_2il0floatpacket.52(%rip), %xmm7 #
movsd .L_2il0floatpacket.46(%rip), %xmm6 #
movsd .L_2il0floatpacket.47(%rip), %xmm5 #
movsd .L_2il0floatpacket.48(%rip), %xmm4 #
movsd .L_2il0floatpacket.49(%rip), %xmm3 #
testq %r12, %r12 #796.13
jle ..B10.15 # Prob 2% #796.13
..B10.13: # Preds ..B10.25 ..B10.13
movsd (%r14,%rcx,8), %xmm10 #857.35
movaps %xmm2, %xmm15 #864.16
xorq %rax, %rax #863.16
comisd %xmm2, %xmm10 #863.16
setbe %al #863.16
incq %rax #863.16
imulq -248(%rbp), %rax #863.22
addq -240(%rbp), %rax #863.16
movsd 16(%rax,%rdx), %xmm11 #866.24
mulsd %xmm2, %xmm11 #866.16
movsd 8(%rax,%rdx), %xmm10 #863.16
movsd 24(%rax,%rdx), %xmm12 #870.24
mulsd %xmm10, %xmm15 #864.16
addsd %xmm11, %xmm10 #867.16
mulsd %xmm6, %xmm11 #868.32
mulsd -232(%rbp), %xmm12 #870.16
mulsd %xmm2, %xmm11 #868.40
addsd %xmm12, %xmm10 #871.16
mulsd %xmm5, %xmm12 #872.32
addsd %xmm11, %xmm15 #868.16
mulsd %xmm2, %xmm12 #872.40
movsd 32(%rax,%rdx), %xmm13 #874.24
addsd %xmm12, %xmm15 #872.16
mulsd %xmm1, %xmm13 #874.16
movsd 40(%rax,%rdx), %xmm14 #878.24
addsd %xmm13, %xmm10 #875.16
mulsd %xmm4, %xmm13 #876.32
mulsd %xmm0, %xmm14 #878.16
mulsd %xmm2, %xmm13 #876.40
addsd %xmm14, %xmm10 #879.16
mulsd %xmm3, %xmm14 #880.32
addsd %xmm13, %xmm15 #876.16
mulsd %xmm2, %xmm14 #880.40
subsd %xmm7, %xmm10 #883.16
mulsd -8(%rbx,%rcx,8), %xmm10 #885.30
addsd %xmm14, %xmm15 #880.26
addsd 48(%rax,%rdx), %xmm15 #880.16
addq %r15, %rdx #886.13
addsd %xmm10, %xmm9 #885.16
mulsd -8(%rbx,%rcx,8), %xmm15 #884.30
incq %rcx #886.13
addsd %xmm15, %xmm8 #884.16
cmpq %r12, %rcx #886.13
jle ..B10.13 # Prob 82% #886.13
有问题END DO
的说明是:
addq %r15, %rdx #886.13
incq %rcx #886.13
cmpq %r12, %rcx #886.13
jle ..B10.13 # Prob 82% #886.13
在分析中,几乎没有时间花在addq
上,75%的时间花在incq
上。
答案 0 :(得分:1)
可能发生的事情是周期会被错误的指令收费。分析通常有点不精确。或者周期因为OOO引擎的跟踪资源已满而停止等待发布的指令。
指令必须按顺序退出,因此一条旧指令(例如缓存未命中加载或存储)会在the out-of-order window has gone as far beyond it as possible时停止管道。
我假设在该循环的FP变量中有一个长循环传递的依赖链,因为没有存储,只有加载。并且我假设在顶部的(无分支)比较取决于prev迭代的结果。因此,每次迭代都必须等待前一次的结果执行。但是循环计数器指令不是这个依赖链的一部分,所以它们可以远远超过循环体的进程。 (如果有任何加载的地址只依赖于循环计数器,它们可能会远远超过使用它们的代码。)
所以很多时候,循环开销指令将是完成的最新指令,他们只是等待退出,直到超过100 uops的FP依赖链清除掉ROB(re)阶-缓冲液)。
我不确定这是否完全正确,但我认为它比inc
- 慢速理论更好。英特尔手册误导了所有人,这太糟糕了。
inc
不问题,除非您在P4上运行此问题。英特尔手册的那一部分是陈旧的。如果您无法用inc
替换它,则只需要避免使用add
(因为执行需要保留进位标记,例如{{1}循环)。
即使在P4上,adc
也不会成为此代码的问题。一个错误的依赖关系不会使循环速度变慢,以至于OOO执行无法预见未来的迭代。另请注意,在循环的早期有一个inc
,而且你没有说出任何关于热的内容。
请参阅x86 wiki了解优化指南,包括Agner Fog的优秀内容。如果你想知道为什么 inc %rax
在P4上很慢,请阅读他的微体系结构指南。
答案 1 :(得分:0)
正如Jester所指出的那样,INC的使用是有问题的,因为它只对Flag寄存器进行了部分更新,因此它需要它的先前值。这反过来意味着它必须等待最后的标志写入指令才能执行。这通常会通过引入错误的依赖关系来减慢执行速度。
在这种特殊情况下,它会强制不必要地等待前一条指令的结果,以便获得INC未更新的进位标志(CF)的值。如果使用了ADD,则不会一直是对标志的读取依赖,因为ADD只将新值写入所有标志。
在这种情况下,先前的标志写入指令是ADDQ%R15,%RDX,否则INC将不依赖于此。很难说为什么这种依赖应该在这里产生如此巨大的影响(对于像DIV或MUL这样长时间运行的指令会更加明显;但是,像ADDSD和MULSD这样的SIMD指令不会影响标志)。
进位标志的值对于INC之后的指令是完全无关紧要的,因为CMPQ在没有读取的情况下覆盖了标志而且ADDSD并不关心,这就是为什么它被称为“假”&#39 ;依赖性。
即使是最新的Intel® 64 and IA-32 Architectures Optimization Reference Manual也说:
3.5.1.1使用INC和DEC说明
INC和DEC指令仅修改其中的一部分位 国旗登记。这会产生对所有先前写入的依赖 国旗登记。当这些说明时,这尤其成问题 是关键路径,因为它们用于更改地址 对于许多其他指令所依赖的负载。
汇编/编译器编码规则33。 (M impact,H generality)INC和 DEC指令应替换为ADD或SUB指令, 因为ADD和SUB会覆盖所有标志,而INC和DEC则不会, 因此在先前设置的指令上创建错误依赖 旗帜。
这正是Jester所说的,专业的编译器当然应该注意这些编码规则。