perf性能分析(7) -- Top-down 分析方法及VTune工具

现代性能分析,使用针对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的结果被写会寄存器或者内存(经过Store Buffer写入内存)。

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

cpu_micro_arch

在Intel处理器中,一个core一般有四个执行端口,即每个cycle最多可以执行四个uOps

在处理器架构中,有一个抽象概念:pipeline slot流水线槽),用来表示用于执行一个uOp所需要的硬件资源。在每个时钟周期,有四个流水线槽可用,流水线槽可以是空的,也可以是被uOp填充。流水线槽Allocation阶段(上图中的星号标记处),将uOp从前端分配到后端执行单元。

PMU监控流水线槽的状态,在每个时钟周期,衡量流水线槽的利用率(是否填充有uOp),并对流水线槽进行分类,确定是前端瓶颈还是后端瓶颈。

3. Top-down 分析方法

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

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

Top-down分析思想根据上述的流水执行阶段及过程,首先从Top-level分类分析步骤:

top_level_breakdown_flowchart

然后,继续细分(Breakdown),确定是哪个阶段中的哪个资源导致的stall

top-down-hierarchy

相比以前基于事件的度量的方式,基于Top-down分析方法,可以更准确地定位性能瓶颈的根源,指导开发者进行针对性的优化。

3.1. Frontend Bound – 前端瓶颈

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

而在取指/解码过程中,如果代码的局部性不好,由于ICache Miss,或者iTLB Miss,引起前端Stall。过高的miss率导致前端瓶颈。

L1/L2 Cache分为DCacheICacheTLB也分为iTLBdTLBSecond-Level TLB,地址从iTLB/dTLB中找不到则再从Second-Level TLB中查找。

3.2. Back-End Bound – 后端瓶颈

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

  • Core Bound
    • 除法指令过多,因为除法指令执行时间长;
    • Execution Port Utilzation过高,导致执行单元饱和;
    • 指令的数据依赖,即依赖上一个指令的结果,导致Stall
  • Memory Bound
    • L1/L2/L3 Cache Miss引起的Stall,需要改进数据访问的局部性,或者减小数据的访问规模(如分块处理数据),或者False-Sharing引起的Cache Miss
    • Memory Bound:分为Memory BandWidthMemory Latency,前者是指内存带宽不足,后者是指内存访问延迟过高;

3.3. 优化指令的处理能力(Throughput)

例如,改用向量指令(如AVX)。或者使用更高效的算法。

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. 示例

#define likely(x) __builtin_expect(!!(x), 1) //gcc内置函数, 帮助编译器分支优化
 #define unlikely(x) __builtin_expect(!!(x), 0)

 if(likely(condition)) {
   // 这里的代码执行的概率比较高
 }
 if(unlikely(condition)) {
  // 这里的代码执行的概率比较高
 }
#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 – 计算机组成原理——原理篇 处理器(中)

6. 使用VTune工具进行Top-down分析

对一个应用程序进行profile,一般首先使用Hotspots,获取测量得到的分类性能指标。以官方matrix_mul示例为例(以Windows为例,VTune自带样例的路径为C:\Users\Administrator\Documents\VTune\Samples):

demo_hotspots demo_hotspots_vec

然后,使用Microarchitecture Exploration,再次进行测试。测试完成之后,在Bottom-up页面查看Top-down Tree,获取更细粒度的性能指标:

demo_top_down_tree demo_top_down_tree_frontend_backend_zoomin demo_top_down_tree_backend_zoomin2

  1. 下图为局部展开的Front-End BoundBack-End Bound
  2. 从图中看,Front-End Bound44.0%,即流水线槽Pipeline Slot)由于前端没能及时提供指令而产生44.0%的空闲。
  3. Back-End Bound100.0%,意味着后端由于内存带宽限制、指令的数据依赖(比如指令依赖上一条指令的计算结果)、L1/L2/L3缓存访问延迟严重,或者执行单元饱和,导致流水线停顿。
  4. 综合前后端的瓶颈分析,前端应该是由于后端的瓶颈导致的。

6.1. Back-End Bound

Top-down的顶层分类中,使用流水线槽Pipeline-Slot)来测量瓶颈:Front-End BoundBack-End BoundBad SpeculationRetiring。在深入到Back-End之后,使用PMU来测量后端的性能指标值,比如测量一个指令的时钟周期或者事件,进而度量其Stall来源并算比例值。其测量值,与使用流水线槽度量及计算得到的值会有差异。

另外,Back-End Bound又分为两个子类:Memory BoundCore Bound,分别度量来自内存/Cache的Stall,以及来自Execution PortStall

6.2. 微架构优化方法

在进行性能优化的时候,一般关注应用程序的顶层热点函数,即占用CPU最多的函数。VTune工具展示的profile结果也是热点 + 层次定位,比如Bottom-up页面,可以按照占用CPU时间排序查看,即查看热点函数以及其耗时占比。

按照最优优化顺序,首先优化程序算法,以及并行化,即优化大头。之后,再进行微架构性能profile,并按照Top-down方法,针对性的优化瓶颈。

在Top-down视图中,VTune会高亮显示Top-down分类中的瓶颈类型(如上面Bottom-up页面所示)。下表给出了不同分类应程序,其Top-down分类中各指标的合理范围,超出该范围5%则在VTuneprofile结果页面中高亮显示:

top_down_threshold

  1. 这个阈值表是基于对Intel实验室中的一些工作负载的分析总结出来的,作为一般指导原则。
  2. 在Top-down分析过程中,没有高亮的指标,不需要花费时间优化,优化这一部分一般不会有太多的改善。
  3. 大多数未调优的应用程序都是后端瓶颈的。 解决后端问题通常与解决延迟源有关,延迟源会导致执行完成所需的时间超过必要时间。

6.3. 第三方优化示例

A. 资料

A.1. 更多阅读




    Enjoy Reading This Article?

    Here are some more articles you might like to read next:

  • 在Ubuntu上部署OpenMAIC
  • AI工具大全
  • Fast DDS入门(On-Going)
  • NVIDIA GPU 架构:SP、SM 与 LSU 工作原理详解
  • al-folio 模板定制修改总结