从Agner Fog's "Optimizing Assembly" guide开始,第12.7节:循环示例。讨论示例代码的其中一段:
[...] Pentium M的分析:......每时钟3个微处理器13个微处理器=每4.33c退休时间一次迭代。
循环中有一个依赖链。延迟时间为:2为 存储器读取,5表示乘法,3表示减法,3表示存储器 写入,总共13个时钟周期。这是原来的三倍 退休时间,但它不是一个循环携带的依赖,因为 每次迭代的结果都保存到内存中,不会重复使用 下一次迭代。乱序执行机制和 流水线操作使得每次计算都可以在之前开始 完成上述计算。唯一的循环携带 依赖链是
add eax,16
,其延迟只有1。
## Example 12.6b. DAXPY algorithm, 32-bit mode
[...] ; not shown: initialize some regs before the loop
L1:
movapd xmm1, [esi+eax] ; X[i], X[i+1]
mulpd xmm1, xmm2 ; X[i] * DA, X[i+1] * DA
movapd xmm0, [edi+eax] ; Y[i], Y[i+1]
subpd xmm0, xmm1 ; Y[i]-X[i]*DA, Y[i+1]-X[i+1]*DA
movapd [edi+eax], xmm0 ; Store result
add eax, 16 ; Add size of two elements to index
cmp eax, ecx ; Compare with n*8
jl L1 ; Loop back
我无法理解为什么依赖链不会增加整个吞吐量。我知道找到最严重的瓶颈很重要。在考虑依赖链之前确定的最严重的瓶颈是融合域uop吞吐量,每次迭代4.33个周期。我无法理解为什么依赖链不是一个比这更大的瓶颈。
我看到作者解释说它与无序执行和流水线有关但我看不到它。我的意思是,只有乘法才会导致延迟5个周期,所以只有这个值大于4个周期。
我也无法理解作者为什么不关心这里的依赖:
add eax, 16 -> cmp eax, ecx -> jl L1
毕竟,必须在cmp
之前执行添加,cmp
之前必须执行jl
。
PS:后面的段落确定了Pentium M作为解码的最大瓶颈,将其限制为每6c一次迭代,因为128b矢量操作解码为每个两个uop。有关Core2,FMA4 Bulldozer和Sandybridge的其余分析,分析和调整,请参阅Agner Fog指南。
答案 0 :(得分:2)
mul不是循环携带依赖关系链的一部分,因此可以同时在多个迭代中进行mulpd
个insn。单个指令的延迟根本不是问题,它是依赖链。每次迭代都有一个单独的 13c依赖性加载链,mulpd,subpd,store。乱序执行允许多次迭代的uops同时在飞行中。
每次迭代中的cmp
/ jl
取决于该迭代中的add
,但下一次迭代中的add
不依赖于cmp
。推测执行和分支预测意味着控制依赖(条件分支和间接跳转/调用)不数据依赖链的一部分。这就是为什么一次迭代的指令可以在前一次迭代的jl
退出之前开始运行的原因。
相比之下,cmov
是数据依赖而不是控件依赖,因此无分支循环往往具有循环携带的依赖链。如果分支预测良好,这往往比分支慢。
每个循环迭代都有一个单独的cmp
/ jl
依赖链,就像FP依赖链一样。
我无法理解为什么依赖链不会增加整个吞吐量。
我不知道这句话是什么意思。我想我能够找出你所有其他混淆的单词和短语。 (例如“链依赖”而不是“依赖链”。)看看我对你的问题的编辑;其中一些也可能有助于你理解。