顶层划分
在Top-Down
最开始的时候,我们需要考虑如何做最顶层的划分。Top-Down
方法按照如下的方式对微指令做划分:
Top Level breakdown flowchart
对于一个已经被发射的微指令,那么它最终只有两个结果:Retired
或者Cancelled
。因此,这条指令要么被计入到Retiring
阶段或者Bad Speculation
阶段。反过来,对于一个还没有分配的微指令,如果出现了Backend Stall
,后端由于一些原因无法处理相关的指令,则计入到Backend Bound
;反之则将其计入到Frontend Bound
。
Frontend Bound
前端主要承担以下工作:
- 基于分支预测取出下一条地址
- 取出
cache line
解析指令
- 将指令解析成微指令
处理前端的问题会有一些困难,因为它们出现在流水线的最开始,这些短暂的问题可能并不是导致问题的真正原因。因此,我们只需要在Frontend Bound
被标记的时候进行下钻分析即可。Top-Down
更进一步的将Frontend Bound
划分成为延时(latency
)和带宽(bandwidth
)两个维度。icache miss
、 iTLB miss
和 Branch Resteers
都属于Frontend Latency
问题,而前端解码器的效率问题会被划分到Frontend Bandwidth
维度。Branch Resteers
记录了流水线冲刷(pipeline flush
)
Top-Down
更进一步的将带宽按照取指(Fetch
)单元做了划分。一般的取指单元会被划分到Fetch src 1
,而涉及到像CPUID这样复杂指令的解析,可能会使用到Fetch src 2
。
Bad Speculation
Bad Speculation
反映了流水线槽(slots
)因为错误的投机预测而被浪费的情况。Bad Speculation
包含两种情况:因为错误的投机操作执行最终不会Retired的指令,如Branch Misspredicts
;或者流水线因为前面的投机操作而被阻塞。比如在分支预测窗口里被丢弃的指令就会被计入到这里面。
注意分支预测的第三种可能的处理会和获取正确的目标的速度有关。如果其会导致其他的前端阻塞的话,会被计入到Frontend Bound中的Branch Resteers中。
Bad Speculation
是Top-Down
分析中很重要的一部分。其能够帮助我们了解到预测错误对工作的影响,从而反过来决定其他几个部分的准确性。Bad Speculation
进一步分成了 Branch Mispredict
和 Machine Clears
,后者的情况导致的问题与 pipeline flush
类似:
Branch Mispredict
关注如何使程序控制流对分支预测更友好
Machine Clears
指出一些异常情况,例如清除内存排序机(Memory Ordering Nukes clears
)、自修改代码(self modifying code
)或者非法地址访问(certain loads to illegal address ranges
)
Retiring
Retiring
的部分表示了所谓的好指令,也就是正常跑完的指令。理想状态下,我们希望所有的指令都能够Retiring
,这样的话就可以完全的发挥CPU的作用。假设每条指令会生成一条微指令,在一个四发射机器上如果能有50%的Retiring
率,那么相应的IPC就是2。
但是,很高的Retiring率并不意味着就没有性能优化空间了。例如:
Micro Sequencer
指标中的Floating Point
操作就对性能不友好,我们应该尽量避免使用;
- 对于非向量操作而言,很高的
Retiring
率意味着我们可以尝试将其修改为向量操作;
Backend Bound
Backend Bound
表明了后端并没有足够的资源能够处理发送的微指令的情况。基于执行单元我们将Backend Bound
分成了Memory Bound
和Core Bound
。前者更多的偏向于访存操作,而后者偏向于计算操作。如果想要更好的IPC表现的话,我们应该尽可能的让执行模块处于忙碌中,或者说流水线尽可能忙碌。否则我们将这些周期称为执行停顿(Execution Stalls
)。
流水线
Memory Bound
通常和内存子系统的阻塞有关。这些阻塞通常会导致执行单元一小段时间的饥饿现象。
Core Bound
反映了短的执行饥饿周期或者执行端口利用率不佳,例如一个长延迟的除法操作可能会序列化执行,导致一个周期内只有少量的执行端口被使用。这会让执行单元造成压力,并且缺少指令集并行。
Core Bound
的问题一般可以通过更优秀的代码来解决。编译器也可以通过更好的指令调度来缓解。同时,矢量化(Vectorization
)也可以缓解 Core Bound
问题。
Memory Bound
现代CPU实现了三级缓存结构。在Intel CPU中,第一级缓存实现了数据缓存(L1D
),第二级缓存是核心共享的数据缓存和指令缓存,第三级则是在多个核上共享的缓存。
为了处理叠加的影响,Top-Down
尝试引入了一种启发式的方法来确定内存访问的惩罚。优秀的乱序执行CPU可以通过使用不依赖内存访问的微指令来填满CPU从而避免内存访问带来的阻塞。因此内存访问真正的惩罚是调度器没有准备好微指令给执行模块执行,这些微指令要么在等待内存访问,要么就依赖于其他未执行的微指令。下图表示了如何区分缓存带来的停顿:
Memory Bound breakdown flowchart
例如,L1D缓存拥有和ALU阻塞媲美的短延迟。但是在实际场景中, load
操作被阻塞,无法将数据从早先的 store
转发到一个相同的地址可能会导致较高的延迟操作。这种情况会被归类到图中的L1 Bound中去。
在乱序执行的CPU上,存储(Store
)操作会被缓存并在指令Retiring之后进行。多数情况下这些操作对性能没有影响,但是我们也不能因此而忽视它。所以定义了Stores Bound
,这种情况下执行端口利用率会很低,并且有很多的存储缓存。
数据TLB miss
可以被归类到对应的内存范畴子节点下。例如L1D
的TLB
未命中会被归类到L1 Bound
下。
最后,Top-Down
使用了一种启发式的方法来区分Ext.Memory Bound
下的Memory BandWidth
和Memory Latency
。首先统计有多少请求依赖从内存中获取数据,如果该数值超过一个阈值,则定义为Memory Bandwidth
,否则就归类为Memory Latency
。