文章

perf性能分析(7) -- Top-down 分析方法

现代性能分析,使用针对pipeline的分析办法(取代CPU cycles分析)。这源于现代CPU架构的复杂性。

现代CPU处理指令架构,分为前端 Front-end后端 Back-end两部分。阻碍指令执行的因素,从硬件看,源于前端后端Stall

1. CPU 流水线

intel_five-stage-pipeline

Intel CPU流水线一般分为5级。其中解码(ID),意思是将指令操作分解为多个uOp(即拆分为多个更低级的硬件操作),如ADD eax, [mem1],可以拆分成两个微指令:从内存读取数据,再执行ADD操作。

2. CPU 架构及流水线的执行过程

执行过程:前端执行完IF -> ID之后,然后在一个名为allocation的过程中,uOps被输送到后端后端监控uOp的操作数(data operand)何时可用,并在可用的执行单元中执行uOps。 当uOp执行完成之后,称之为执行完成(retirement),且uOp的结果被写会寄存器或者内存。

大多数uOps都会完全通过流水线并退出,但有些投机指令uOps可能会在退出前被取消–如预测错误的分支。

cpu_micro_arch

在处理器架构中,有一个抽象概念:pipeline slot,即执行端口。在Intel处理器中,一个core一般有四个执行端口,即每个cycle最多可以执行四个uOpsVTuneallocation阶段,可以测量pipeline slot的利用率(星号标注的地方)。

3. Top-down 分析方法

从性能分析的角度看,一条微指令在流水线中的性能指标可以分为:

  • 退出(Retiring) – Micro Sequencer(微指令调度器)可能会成为瓶颈,例如调度浮点指令。
  • 分支预测错误(Bad Speculation) – 分支预测错误,或者memory ordering violation(多核多线程共享数据情形),导致Machine Clears(清除流水线)。
  • 前端瓶颈(Front-End Bottleneck)
  • 后端瓶颈(Back-End Bottleneck)

top-down-hierarchy

顶级分类分析步骤:

top_level_breakdown_flowchart

3.1 Frontend Bound – 前端瓶颈

前端主要职责为读取指令,解码之后,发送给后端。遇到分支指令,需要经过预测器预测下一个指令的地址,这意味着会出现由于分支预测错误导致的ICache Miss而引起前端阻塞。

3.2 Back-End Bound – 后端瓶颈

后端主要职责为执行指令,包括ALUFPUMemory等。后端瓶颈分为:

  • Core Bound
  • Memory Bound

D Cache Miss,浮点除法器过载,指令依赖、数据依赖等。

4. 一些优化手段

4.1 Frontend

  1. 减少代码的footprint,如-fomit-frame-pointer
  2. 调整代码布局:如是用-fprofile-generate -fprofile-use
  3. 调整代码布局:如用__attribute__((hot))
  4. 分支预测优化:如loop unrolling,特别是小的循环,如小于64次循环
  5. 分支预测优化:是用if代替三目运算符;避免if-elses结构,switch-case排序

4.2 Back-End

  1. 减少function call,如inline
  2. 多线程避免false-sharing,使用内存对齐
  3. gcc优化:如__builtin_expect

4.3 示例

1
2
3
4
5
6
7
8
9
#define likely(x) __builtin_expect(!!(x), 1) //gcc内置函数, 帮助编译器分支优化
 #define unlikely(x) __builtin_expect(!!(x), 0)
  
 if(likely(condition)) {
   // 这里的代码执行的概率比较高
 }
 if(unlikely(condition)) {
  // 这里的代码执行的概率比较高
 }
1
2
3
4
5
6
7
8
#define CACHE_LINE __attribute__((aligned(64)))

struct S1 {
    int r1;
    int r2;
    int r3;
    S1(): r1(1),r2(2),r3(3){}
  } CACHE_LINE;

5. Hazard 介绍

5.1 StructuralHazards – 结构性冲突

结构性冲突本质是CPU中硬件资源的竞争,比如流水线中,前后指令之间都需要经过译码器,访问内存,形成对译码器的争用。

5.2 DataHazards – 数据依赖

五级流水线:取指IF -> 解码ID -> 执行EX -> 内存访问MEM -> 写回WB

Data Hazard是指后一条指令的操作数,依赖于前一条指令的结果。操作数依赖分为三种关系:

  • 先写后读(Write-after-Read) – Data Denpendency
  • 先读后写(Read-after-Write) – Anti-Dependency
  • 写后写(Write-after-Write) – Output Dependency

CPU处理Data Hazard办法有两种:

  • 插入NOP指令,流水线停顿(Pipeline Stall),或者叫流水线冒泡(Pipeline Bubbling)。
  • Operand Forwarding – 操作数转发。

Operand Forwarding:在第一条指令的执行阶段完成之后,直接将结果数据传输给到下一条指令的 ALU。然后,下一条指令不需要再插入两个 NOP 阶段,就可以继续正常走到执行阶段。这样的解决方案,我们就叫作操作数前推(Operand Forwarding),或者操作数旁路(Operand Bypassing)。其实更合适的名字应该叫操作数转发。这里的 Forward,其实就是我们写 Email 时的“转发”(Forward)的意思。

pipeline_bubbling operand_forwarding

5.3 ControlHazards – 控制依赖

主要使用分支预测。

5.4 流水线 – 乱序执行

instruction_out_of_order

更详细资料:cnblogs – 计算机组成原理——原理篇 处理器(中)

资料

更多阅读

本文由作者按照 CC BY 4.0 进行授权