本文翻译自:Arm Neoverse N1 Core: Performance Analysis
Methodology Performance Analysis on Neoverse N1 Core Using Hardware PMU Events https://www.arm.com/-/media/Files/pdf/white-paper/neoverse-n1-core-performance-v2.pdf
原作者:
Jumana Mundichipparakkal,
Krishnendra Nathella,
Tanvir Ahmed Khan
翻译并稍做扩展:Zenon Xiu
1 简介
随着许多 Arm 硬件和软件合作伙伴开发应用程序并将其工作负载移植到基于 Arm 的云实例上,Arm Neoverse 生态系统正在快速发展。随着 Neoverse N1 系统的广泛可用,与传统系统相比,许多真实世界的工作负载显示出非常有竞争力的性能和显著的成本节省。一些最近的例子包括:H.264 视频编码、memcached、Elasticsearch、NGINX 等等。
为了最好性能,开发人员使用性能分析和工作负载特性分析技术来研究应用程序的性能特征。服务器系统支持广泛的性能监测技术,用来监测工作负载效率、评估其资源需求并跟踪资源利用率。这些监测对调整软件和硬件非常有用,还有助于指导未来的系统设计。
Arm Neoverse 微架构的开发需要考虑到高性能和低功耗。因此,我们的性能监测方法可能与软件开发人员用于分析其他架构的方法略有不同。本文介绍了一种使用 Neoverse N1 CPU 上的性能监测单元(PMU)进行工作负载特性分析的方法,以找出和消除性能瓶颈。本文目标受众是从事软件优化, 软件开发和性能分析师。
本文的内容分为四个章节:
第一章 介绍 Neoverse N1 上的硬件 PMU,列出了最相关的 PMU 事件以进行工作负载特性分析。
第二章 介绍使用 Neoverse N1 CPU核PMU 事件的工作负载特性分析方法。
第三章 介绍如何使用 Linux perf 工具来收集 Neoverse N1 PMU 事件。
最后一章 通过一个工作负载案例研究演示工作负载特性分析和热点分析。
2 Neoverse N1性能监测事件
性能监测需要在系统上收集应用程序执行信息,这可以通过软件或硬件手段获得。软件监测技术提供由系统软件计数的软件跟踪信息和事件(如ftrace)。硬件监测技术通过直接从CPU/系统收集硬件事件来工作。为了收集硬件事件,现代处理器实现了专用的性能监测单元(PMU),它可以监测各种硬件执行相关事件。对硬件事件进行分析可以了解代码执行时各种微构架的行为。可以监测多个事件,并与软件执行相关联,以寻找优化机会,并评估工作负载是否以最优方式利用底层微构架。支持的事件包括已retried指令、CPU cycle、缓存/ TLB访问和分支预测。
Arm性能监测单元(PMU)
Arm架构通过性能监测器扩展的可选扩展支持PMU功能。 Arm PMU硬件设计(图1)由以下组件组成:
- 用于控制和事件选择的PMU配置寄存器
- PMU事件计数器
- 专用功能计数器
PMU硬件有多个配置寄存器,包括PMCR和PMEVTYPER寄存器,用于单元控制和监测事件选择。PMU硬件还包括一组事件计数器,以计数用户需要的原始硬件事件。每个事件都有关联的唯一十六进制事件代码(eventcode),可以设置在PMEVTYPER寄存器。除了可配置计数器外,AArch64还具有一个专用计数CPU cycle的计数器,这是所有Arm兼容设计都具有的功能。PMU硬件可用的计数器数量,可以计数的事件类型以及相关的事件代码是CPU硬件实现决定的。
当配置PMU事件时,其中一个事件计数器被分配给与被监测事件。PMU事件可以使用Linux perf工具进行监测,可以通过指定共有的“hardware events”事件名称或指定的原始“PMU硬件事件”关联的十六进制事件代码进行监测。一个典型的处理器可以支持数百个性能和调试目的的事件,但一个时刻只能同时计数一部分事件,用于工作负载特征化提取,以识别瓶颈和资源利用率。一旦从特征化提取中确定了性能瓶颈,可用其余CPU核子系统特定的事件深入排查性能问题。
Arm性能监视器单元实现参考文档
CPU核实现相关的事件会在该产品的技术参考手册列出。 Arm架构参考手册定义了“Common Events”的行为,Common Events适用于所有微架构,但其中大部分是软件要求,可能PMU硬件不一定实现。
Neoverse N1性能监视器单元
Neoverse N1 CPU实现了Arm v8.1 PMU扩展,支持100多个硬件事件。Neoverse N1 PMU具有6个可配置计数器和1个专用计数CPU cycle的功能计数器。
Neoverse N1 CPU核实现的PMU事件在ARM Neoverse N1 Core技术参考手册(TRM)Part D2中列出。这些事件在Arm v8.1中定义为“Common Events”。除了TRM,我们还提供Neoverse N1 PMU Guide,这是一个补充文档,用于描述CPU核实现的硬件PMU事件。该文档提供了按CPU功能模块分类的PMU事件的详细描述。。
为了更好理解每个PMU事件代表的微构架和体系结构意义,PMU事件描述中提供了相关定义的参考。Neoverse N1 PMU Guide还添加了一个专门的CPU执行流程章节,描述了内存子系统的关键CPU执行流程,也描述了每个stage计数的PMU事件。
我们建议使用N1 PMU Guide作为性能分析过程中使用PMU事件描述的首选参考手册。
用于工作负载特征提取的Neoverse N1PMU事件清单
虽然Neoverse N1 CPU核支持100多个硬件计数器,但并不需要所有计数器都用于工作负载执行的初始特征提取。图2是Neoverse N1 CPU首次进行工作负载特征提取时的主要性能监控事件清单
3 Neoverse N1 性能分析方法
今天,性能优化和软件调优比以往任何时候都更具挑战性。
现代高端处理器通常具有很多CPU核、复杂指令集、各种并行化和更深的内存层次结构。此外,很多大规模云工作负载运行在虚拟机上,这使得在数据中心上追踪工作负载执行行为以评估功耗-性能变得更加困难。然而,将关联软件执行与微构架行为,对优化软件以在基础硬件上高效执行至关重要。
单个CPU核是处理器(多核处理器)的基本执行单元。CPU核与内存子系统的多种配置组成计算系统。
在本章中,我们将研究单个 Neoverse N1 CPU核的高级细节和使用CPU核硬件 PMU 事件进行工作负载特制提取的方法。
Neoverse N1 CPU核
Neoverse N1 CPU核是一种乱序超标量CPU,可以每个cycle最多dispatch/retire 8 条指令。超标量处理器在其流水线中具有三个主要阶段:In-order Fetch和Decode、乱序执行操作(out of order execution)以及最后in-order commit/retire指令。In-order Fetch和Decode部分称为前端(Front End),乱序执行部分称为后端(Back End)。CPU 还有内存子系统,负责所有内存操作及其有序执行。所有这些主要 CPU 模块的设计细节因微构架而异。
在本文档中,我们将描述大多数超标量架构中常见的模块,并着重于与 N1 实现相关的性能指标。有关 Neoverse N1 微构架的详细说明,请参阅 Neoverse N1 技术参考手册和 Neoverse 软件优化手册。下图 3 展现了 Neoverse N1 CPU 的大体模块。
前端(Front End)
CPU的前端是一个按顺序(in-order)处理流水线,负责从I-Cache中获取指令,解码这些指令,并将它们排队送到后端的执行引擎。在解码阶段,架构指令可以被分解成微操作(micro-operations)。这些微操作会根据它们的可用性被排队和分派到执行引擎。除了Fetch, Decode和Dispatch单元外,还有一个重命名(Renaming)单元,用于跟踪操作数据流和依赖关系,以确保执行的操作按顺序提交(commit)。前端中另一个重要的单元是分支跳转预测器。它预测分支跳转的方向以及间接分支跳转的目标地址。请注意,乱序处理器可以预先Fetch多条指令以填充流水线并进行推测性执行。分支预测技术有助于尽可能正确地获取指令流,因为分支预测误判会导致流水线冲刷和浪费cycle。Neoverse N1每个cycle可以Fetch最多4个指令并Dispatch最多8个微操作。
后端 (Back End)
微操作被Dispatch到相关CPU的后端处理执行单元进行处理。Neoverse N1支持多个执行单元,包括分支单元、load/store单元(LSU)和包括高级向量引擎(advanced vector
engines)在内的算术单元(ALU)。执行单元的 数量和cycle数因微架构而异;因此,指令执行延迟和吞吐量取决于CPU硬件实现。一旦指令执行完成,结果就会在依赖关系解决时按顺序存储和提交。
Neoverse N1具有4个整数执行单元、2个浮点数/SIMD流水线和2个load/store流水线,允许每个cycle将最多8个微操作分派到执行流水线中。
内存子系统
CPU的内存子系统处理load、store操作,这在很大程度上依赖于内存层次结构。Neoverse N1每个CPU核都有专有的L1/L2缓存 (cache),其中L2缓存在L1数据缓存和L1指令缓存之间共享。load/store unit控制缓存和到内存的数据流。
Neoverse N1具有两个load/store unit,均可处理读写操作。L1数据缓存是一个64kB 4路组相联设计,而L2缓存是一个8路组相联缓存,大小可根据实现进行配置,最大可达1 MB。CPU核的专用L2缓存通过AMBA 5 CHI接口连接到系统的其余部分。
Neoverse N1 cluster配置
Neoverse N1 系统有不同的配置,这取决于互连和cluster/system level cache的实现选择。下面的图4和图5展示了一些Neoverse N1系统的可选配置。
图4展示了一个配置,其中多个CPU核可以配置在基于Dynamic Sharing Unit(DSU)的cluster系统中,每个cluster中有两个CPU核。此DSU cluster包含一个snoop filter和一个cluster内CPU核共享的可选L3 cluster缓存。此可选L3缓存在设计上可以达到2 MB大小。
另一种配置是直接连接系统(direct connect),如图5所示,顾名思义,CPU核直接连接到一致性网格互连(Coherent Mesh Interconnect)接口CAL。这些系统不支持DSU cluster L3缓存.
所有具有一致性网格互连的系统都支持一个共享的系统级缓存(system level cache),其大小可以高达256 MB。了解缓存层次结构和被分析的系统配置对从缓存有效性PMU事件中得到见解至关重要。最好向芯片提供商咨询有关底层系统的详细配置,包括缓存大小。
性能分析方法论
对于工作负载分析,既可以使用原始硬件事件,也可以使用从中派生出来的一些有用的指标进行分析。理解所有event并决定使用哪些事件是一项非常棘手的任务,因为每个工作负载都具有独特的行为和潜在瓶颈。此外,要精确定位到特定的硬件问题,可能需要具有深入的微构架知识。为了简化此过程,我们将重点介绍一些原始事件和派生指标,这些指标可以帮助进行工作负载的初步特征。利用这个过程应该有助于获取工作负载的顶层特征,并在后续深度分析过程中找到性能问题的根源。
可以利用的基本顶层分析方法论始于执行过程中的cycle计数,如图6所示。
每cycle指令数(IPC)
消耗的cycle数和执行的指令数量为任何体系结构上的 CPU 工作负载和执行时间提供了概况。指令数量代表 CPU 所做的工作量,而cycle代表完成这些工作所需的总时间。每cycle指令数(IPC)是评估工作负载在微构架上性能的关键指标,可以如下计算:
在非停顿(non-stalled)流水线中,IPC 将是最高的,可以直接监测处理器支持的指令级并行性。IPC 达到的越高,执行过程中流水线的效率就越好。如果 IPC 非常低,这意味着花费的cycle大量停顿,这可能导致潜在的性能问题。
Neoverse N1 流水线的最大 IPC 为 4,因为它可以每个cycle获取 4 条指令。对于指令计数,Neoverse N1 支持 INST\_RETIRED 和 INST\_SPEC 事件,两者都在流水线的不同阶段计算执行的指令数。虽然 INST\_RETIRED 计数程序中体系结构上执行的总指令数,但 INST\_SPEC 则计数被解码的指令总数。INST\_SPEC 可以更好地指示执行单元的总利用率,因为它计算的是可能已经被执行但不一定被提交的指令。INST\_SPEC 和 INST\_RETIRED 之间的差异通常表明分支误预测率很高或其他fault,这些fault会取消已被解码的推测指令,降低效率。
请注意,对于工作负载,INST\_RETIRED 在体系结构的所有实现上都是相同的,而cycle则是微构架和系统设计紧密相关的。
cycle计数/流水线停顿
如上所述[Neoverse N1 Core],乱序CPU具有顺序前端单元,该单元获取指令并将其解码为
发出到后端进行执行的微操作。前端中的Fetch单元从L1指令(L1I)缓存中获取指令,这需要访问L1I单元从L1I缓存中获取指令,这需要访问L1I TLB以获取物理地址,和分支预测单元以猜测性地获取后续指令。虽然后端可以乱序执行微操作,但是所有指令都按顺序提交,以便于解决依赖关系。上述流水线允许高吞吐量的指令执行。
但是,由于各种原因,如由于分支预测错误而预取了错误代码路径,或等待来自内存或L2/L3缓存的数据,可能导致流水线中出现停顿。此外,执行过多的错误路径代码会减少CPU最终有用cycle数。
为了评估流水线执行的效率,Neoverse N1支持两个顶层STALL事件,以评估CPU前端和后端停顿的cycle数。
可以使用STALL\_FRONTEND和STALL\_BACKEND事件来指示工作负载执行过程中主要瓶颈。
使用这两个事件,我们可以推导出前端和后端停顿的相对百分比:
一个较高的前端停顿率表明由于有序前端单元中的流水线停顿而浪费了cycle,而相对较高的后端停顿率表明由于后端中的流水线停顿而浪费了cycle。这种分解有助于缩小分析性能问题主要来源的范围,进一步分析以确定性能瓶颈,如图6所示。以下是分析前端受限的工作负载,根据CPU模块/单元进一步分解的列表:
- ITLB事件
- I-Cache事件:L1I + L2/最后一级缓存事件
- 分支效率事件
以下是分析后端受限的工作负载,根据CPU模块/单元进一步分解的列表:
- DTLB事件
- 内存系统相关事件
- D-Cache计数器:L1D + L2/最后一级缓存事件
- 指令组成
N1 PMU指南详细说明了每个CPU模块可以参考的所有原始硬件事件。我们将涵盖从N1 PMU CheatSheet[第2章]推荐的事件,包括可用于表征CPU模块的一些指标。
分支跳转效率
在深度流水线CPU中,分支预测错误很昂贵,导致频繁的流水线冲刷和浪费的cycle。一般规律是,工作负载通常平均每6个指令包含1个分支。虽然现代CPU已经优化了分支预测单元,但有很多用例,如光线追踪、决策树算法等,分支密集且难以预测。其中一些应用可以有数百个独立分支路径可以跳转,而跳转目标地址可能依赖于输入数据。
可以通过使用两个原始PMU事件BR\_MIS\_PRED\_RETIRED和BR\_RETIRED来评估分支预测性能。BR\_MIS\_PRED\_RETIRED记录了执行但被错误预测的总分支数。这意味着在错误预测的代码段中,预测的代码路径方向是错误的,路径中的后续操作是无用的浪费,导致流水线冲刷。BR\_RETIRED计数CPU在架构上执行的总分支数。
两个可以做为分支执行相对整个程序性能指标:
对于分支执行相对整体程序执行性能的高级评估,可以得出两个性能度量标准:Branch MPKI 它提供每千条指令的分支错误预测总数,而 Branch Mis-prediction Rate 给出被错误预测的分支数占总分支数的比率。这两个指标都可以使用 perf record 进一步研究,以确定哪些函数导致分支错误预测率增加
不同类型分支及其预测性能
分支预测单元根据分支类型采用不同的工作方式。它有三个主要组件:
- 分支历史表(Branch History Table,BHT),用于存储条件分支的历史记录,包括是否被taken。
- 分支目标缓冲区(Branch Target Buffer,BTB),用于存储间接分支的目标地址。
- 返回地址栈(Return Address Stack,RAS),用于存储函数返回地址。
Neoverse N1支持三个事件,即BR\_IMMED\_SPEC,BR\_RETURN\_SPEC和BR\_INDIRECT\_SPEC,分类地用于立即、间接和函数返回分支。了解分支类型的细节可以深入探究分支预测单元中每个子模块的性能。请注意,这些事件计数正确预测和错误预测的分支数。不支持仅计数错误预测分支事件。在Neoverse N1上,可以使用Statistical
Profiling Extensions(SPE)将分支错误预测定位到单个分支,这能比仅使用PMU事件进行更有针对性分析问题。
TLB / MMU 效率
另一个重要的性能评估步骤是检查虚拟内存系统的性能,它影响前端指令Fetch性能和数据端的内存访问性能。在处理器访问相应的高速缓存之前,需要将虚拟地址转换为物理地址以进行任何指令/数据内存访问。请注意,程序的内存视图是虚拟地址,但在访问高速缓存或内存时,处理器使用物理地址进行操作。
虚拟地址到物理地址的映射定义在页表中,这些表位于系统内存中。访问这些表需要一个或多个内存访问,需要多个周期才能完成 - 这被称为页表查找。然而,为了加快地址转换速度,利用Translation Lookaside Buffers(TLB)缓存页表查找,大大减少了对系统内存的访问次数。
Neoverse N1实现了一个两级TLB层次结构。第一级包含独立的专有TLB,用于指令和数据(load/store)地址转换。这些TLB的总访问次数分别由L1I\_TLB和L1D\_TLB计数。第二级包含一个指令和数据共享的L2 TLB。还有相应TLB的REFILL计数器,用于计数这些TLB层次中的refill。由于I-side和D-side TLB未命中而导致的需要进行页表查询的访问次数分别由事件ITLB\_WALK和DTLB\_WALK计数。
为了评估TLB的效率,可以从原始事件中推导出四个指标:
ITLB MPKI和DTLB MPKI分别提供每千条指令的指令和数据访问TLB Walks率,用于评估相对全部指令的TLB效率。
。DTLB Walk Rate提供DTLB Walks与程序总TLB查找次数的比率。请注意,这与DTLB\_WALK / MEM\_ACCESS相同,因为每个MEM\_ACCESS都会导致L1D\_TLB访问。ITLB walk rate提供了从指令侧发起的总TLB查找次数中ITLB walks的百分比。
不同类型指令的混合
Neoverse N1微架构具有8个执行单元,可以处理五种类型的操作:分支、单cycle整数、多cycle整数、带地址生成的load/store单元,以及高级浮点/ SIMD操作。发送到这些执行单元的指令可以通过以下PMU事件进行计数:
- LD\_SPEC:已发出的load指令
- ST\_SPEC:已发出的store指令
- ASE\_SPEC:已发出的高级SIMD/NEON指令
- VFP\_SPEC:已发出的浮点指令
- DP\_SPEC:已发出的整数数据处理指令
- BR\_IMMED\_SPEC:已发出的立即分支指令
- BR\_INDIRECT\_SPEC:已发出的间接分支指令
- BR\_RETURN\_SPEC:已发出的函数返回分支指令
请注意,它们计数的是推测性执行,而不是程序reitire的指令数,因为这些指令在issue阶段被计数,它们可以评估执行单元的利用率。Neoverse N1不支持用于计数架构性执行的retried分类指令数量的事件计数器
。Neoverse N1有PMU事件来进一步将分支操作分解为立即、间接和函数返回分支,
由事件BR\_IMMED\_SPEC,BR\_INDIRECT\_SPEC和BR\_RETURN\_SPEC分别计数。这三个分支操作事件的总和可用做总分支数。
为了评估CPU执行单元的负载,最好从INST\_SPEC计数器的角度推导出每种操作类型的百分比,
CPU核内存流量
MEM\_ACCESS事件计算CPU核的Load Store Unit(LSU)发出的内存操作总数。由于这些操作首先在L1D\_CACHE中进行查找,因此L1D\_CACHE和MEM\_ACCESS事件数量相同。Neoverse N1还支持两个额外的事件,即MEM\_ACCESS\_RD和MEM\_ACCESS\_WR,分别计数读和写流量。请注意,这些事件与LD\_SPEC和ST\_SPEC不同,因为它们计数issue出去的内存操作,但它们不一定执行了。
高速缓存(cache)效率
Neoverse N1实现了多级高速缓存层次结构。第一级(L1)包括一个专用于指令的高速缓存和一个单独的用于数据访问的高速缓存。第二级(L2)是一个共用的L2高速缓存,用于代码和数据之间共享。在此之下,系统可以在CPU cluster中拥有一个可选的L3缓存和一个可选的互连共享系统级缓存(SLC)。L3和SLC缓存是硬件实现可选的。
Neoverse N1 CPU核提供了所有高速缓存层次结构的分层PMU事件。对于每个高速缓存层次,都有总访问计数和refill计数。请注意,AArch64不支持缓存MISS计数器,而只支持REFILL。有的访问操作可能跨cache line边界,这会导致多个cache refill,也有可能多个cache miss使用一个Refill操作。 有关缓存事件计数器的详细描述信息,请参阅N1 PMU指南。缓存策略和相关细节也可以在N1 PMU指南中的微构架详细信息章节中查阅。
可以推导出一组用来研究所有CPU核高速缓存层次结构行为的指标。例如,可以将L1数据高速缓存指标导出为:
CPU核内存流量
MEM\_ACCESS事件计算CPU核的Load Store Unit(LSU)发出的内存操作总数。由于这些操作首先在L1D\_CACHE中进行查找,因此L1D\_CACHE和MEM\_ACCESS事件数量相同。Neoverse N1还支持两个额外的事件,即MEM\_ACCESS\_RD和MEM\_ACCESS\_WR,分别计数读和写流量。请注意,这些事件与LD\_SPEC和ST\_SPEC不同,因为它们计数issue出去的内存操作,但它们不一定执行了。
高速缓存(cache)效率
Neoverse N1实现了多级高速缓存层次结构。第一级(L1)包括一个专用于指令的高速缓存和一个单独的用于数据访问的高速缓存。第二级(L2)是一个共用的L2高速缓存,用于代码和数据之间共享。在此之下,系统可以在CPU cluster中拥有一个可选的L3缓存和一个可选的互连共享系统级缓存(SLC)。L3和SLC缓存是硬件实现可选的。
Neoverse N1 CPU核提供了所有高速缓存层次结构的分层PMU事件。对于每个高速缓存层次,都有总访问计数和refill计数。请注意,AArch64不支持缓存MISS计数器,而只支持REFILL。有的访问操作可能跨cache line边界,这会导致多个cache refill,也有可能多个cache miss使用一个Refill操作。 有关缓存事件计数器的详细描述信息,请参阅N1 PMU指南。缓存策略和相关细节也可以在N1 PMU指南中的微构架详细信息章节中查阅。
可以推导出一组用来研究所有CPU核高速缓存层次结构行为的指标。例如,可以将L1数据高速缓存指标导出为:
缓存(cache)REFILL特征
对于带有DSU cluster的Neoverse N1系统,N1还支持两种缓存REFILL变体,可用于监测缓存refill数据是来自cluster内部还是外部。在这种配置中,REFILL计数可以分为INNER和OUTER refill操作。
远程高速缓存(Remote)访问
对于具有多个Socket或SOC的Neoverse N1系统,N1支持REMOTE\_ACCESS事件,该事件计数来自另一个芯片的数据的内存传输。
最后一级缓存(Last level cache)计数器使用
正如我们在[第3章]N1系统配置部分所看到的,Neoverse N1系统可以支持cluster级别的L3缓存和共享的系统级缓存(shared system level cache)。Neoverse N1实现了两组用于L3和LL(最后一级)的缓存层次结构事件。
L3缓存是可选的,只有当CPU核实现L3缓存时才计数相应的事件。这意味着,如果CPU核没有L3缓存,则此事件应该计数为零。然而,如果系统配置为双CPU核cluster系统,则此事件可以计数来自cluster内部的对等CPU核的snoop传输。[请查看您的SOC规格说明以获取详细信息]
对于支持shared system level cache的系统,LL\_CACHE\_RD计数对SLC的总访问次数。在SLC配置了计数LL\_CACHE\_RD事件的系统中,LL\_CACHE\_RD计数器计数由CPU核进行的总SLC访问,而LL\_CACHE\_MISS\_RD计数器计数在SLC中miss的访问。
为研究最后一级读取行为,可以推导出最后一级缓存读取未命中指标:
另一个有用的度量读SCL流量的命中率是SLC读取命中率。
在Neoverse N1中,最后一级缓存没有写事件的变体,因为SLC仅用作CPU核的eviction cache。
4 使用Linux Perf工具进行性能分析
Linux perf工具是一种广泛使用的开源工具,可以收集源自硬件和系统软件中的性能事件。内核采用perf\_event子系统从处理器本身收集可监测的事件,包括硬件PMU事件。如图7所示,每个CPU核都有自己的专用PMU硬件,内核perf驱动程序单独从每个CPU核的PMU收集事件。
Linux perf\_event系统在Linux内核和用户空间的性能监视工具之间提供了一个接口,用于收集所需的原始硬件事件。Linux perf工具是一种开源工具,在Linux中可用于性能监视,它支持两种监测技术:
计数模式:计数模式在工作负载执行期间收集事件的总体统计信息,每个事件分配的计数器产生事件计数的总数。此事件统计信息有助于分析工作负载执行行为,而不提供有关程序中特定事件发生位置的任何详细信息。对于初始工作负载特征化练习来说,这种方法是最佳途径,以确定工作负载的性能限制。
基于事件采样模式:事件采样模式是一种分析方法,它通过将PMU计数器配置为:在达到预设的事件数量后overflow的方式对每个事件进行采样。在overflow中断处理中记录事件计数,以及指令指针地址(PC)和寄存器信息。这种采样数据用于构建应用程序的分析信息,包括堆栈跟踪和函数级注释(annotation)。有了这些数据,很容易找到对大部分引发采样事件的库和代码部分。
Linux perf 工具中列出的所有性能监测技术,包括计数和事件采样模式,可以通过一下方式使用:
- stat:提供程序整体执行的性能计数器统计信息。
- record:记录执行性能,每个事件的样本百分比,分别匹配到到库和函数。
- report:使用 record 生成记录样本的报告。
- annotate:在代码的反汇编上以样本百分比注释报告。
在需要高精度的情况下,例如当分析热点循环或重要代码段时,应优先选择“计数”模式,因为它更准确,但需要进行多次分析迭代以记录许多不同的事件,以便为每个事件分配一个专用计数器。Neoverse N1 CPU每次最多只计数 6 个事件。当事件数超过可用计数器总数时,计数器在事件之间进行分时复用,并按总时间缩放得到最终计数。虽然这种复用计数可能会导致准确性问题,但通常是可靠的,除非需要精确的监测。
事件采样模式对于对大量代码进行热点分析非常有用,它依靠统计方法在较长的时间或代码段中采样不同的事件。但需要注意,此方法存在一些准确性问题的限制。采样延迟,即计数器overflow和中断处理程序之间的时间差,会导致获得的数据产生飘移,即存储在采样的数据可能并不是事件发生的确切点。另一个问题来自处理器的推测执行,某些执行并触发事件的指令可能无效的,例如它们处于预测错误的代码路径上。尽管这种方法有一些准确性限制,但仍是最接近识别代码执行热点的最佳方法。
如果数据在运行过程中存在大的不一致性,Linux perf 允许调整采样频率,以帮助研究事件计数的变化。
有关如何使用 Liux perf 工具的更多详细信息和示例,请参见 https://www.brendangregg.com/linuxperf.html。
我们将在第 4 章中介绍如何使用 Linux perf 工具对工作负载进行分析和热点分析。
使用 Linux Perf 工具收集 Arm 架构的硬件 PMU 事件
为了开启 PMU 事件收集,必须Linux kernel配置中使用 CONFIG\_HW\_PERF\_EVENTS=y 。大多数产品都启用了此配置选项,但如果您正在编译自定义内核,请记得启用此配置选项。此外,还需要以 root 用户身份配置两个系统设置,以便获得内核符号表和额外的特权。
perf\_event\_paranoid控制影响内核中的特权检查,将其设置为-1允许打开可能会透露敏感信息或可能影响系统稳定性。请参阅内核文档中的详细信息:
https://www.kernel.org/doc/html/latest/admin-guide/perf-security.html#unprivileged-users
https://www.kernel.org/doc/html/latest/admin-guide/sysctl/kernel.html#perf-event-paranoid
kptr\_restrict控制是否公开内核地址(例如,通过/proc/kallsyms)。当不能使用vmlinux(或使用KASLR时)时,有些开发人员使用此技术以获取内核符号表。请参阅有关kptr\_restrict的内核文档的详细信息:https://www.kernel.org/doc/html/latest/admin-guide/
在这两种情况下,都存在潜在的安全隐患,因此在启用它们之前,请查阅官方内核文档并咨询您的系统管理员。
验证PMU事件是否正确计数的简单测试是使用Linux perf工具的perf stat功能计数指令和cycle的总计数。 Perf stat计数指定事件的总数,事件类型是通过16进制寄存器代码提供。 0x8是Arm架构上retired的指令事件的十六进制代码,0x11是CPU cycle的十六进制代码。
这些事件通过带有前缀‘r’的perf stat -e选项提供。
上述命令计数所有CPU上的指令和CPU cycle的总数,持续时间为10秒。 Linux perf允许为特定CPU,每个进程,每个线程计数等,这些可以在perf stat手册页面中找到。 上述perf stat运行的结果如下:
请注意,如果不支持或没启用某个事件,有时perf将悄然失败。 请检查您的CPU TRM以获取有关系统上事件支持的信息。
以下部分中的示例使用原始事件代码指示应监视哪些事件。 然而,Linux内核版本4.17及更高版本支持使用命名值访问N1核PMU事件。 对于早期版本,必须使用原始事件代码。 有关将命名事件映射到代码的信息,请参考Arm Neoverse N1 PMU指南。
对于PMU事件监测的自动化工具,Arm在Github repository库ARM-Software/PMU-Data中提供了所有事件及其事件代码的机器可读的JSON文件。
使用计数模式收集硬件PMU事件
对于计数模式,可以使用Linux Perf工具中的“perf stat”命令,如下所示。
为了对所有事件进行分析,建议按照平台上可用的总计数器寄存器的逐批获取事件。
下面是使用事件名称(添加了十六进制代码)收集指令和cycle的命令行示例:
使用采样模式收集硬件PMU事件
对于采样事件,可以使用Linux Perf工具中的“perf record”命令,如下所示。
有关如何使用这些命令行进行采样和分析采样数据的更多详细信息,请参阅这些Linux perf示例。
一旦使用计数模式收集事件并按照第3章中概述的方法进行工作负载特征分析,就可以从中筛选出一部分事件进行采样和热点分析,如以下例子所示。
5 Neoverse N1 上的性能分析案例研究
在本节中,我们将演示如何使用第三章中概述的性能分析方法,使用从 Linux perf获得的CPU核PMU 指标对 Neoverse N1 系统的工作负载进行特征分析和热点分析。我们在 Neoverse N1 软件开发平台(N1SDP)上运行一个示例工作负载特征分析案例,该平台具有 4 个 Neoverse N1 CPU核。推荐 [第 2 章] 的 PMU 事件使用 Linux Perf 工具“perf stat”一次收集 6 个事件的批次进行收集。
案例研究:DynamoRIO Strided 基准测试
我们运行 DynamoRIO 测试中的 Stride 基准测试。[源参考 https://github.com/DynamoRIO] Stride 微基准测试是一个指针追逐(pointer
chasing)基准测试,它访问一个 16MB 的数组中的值,数组位置由追踪的指针确定。指针位置是在指针追逐kernel运行之前在数组中设置的常量值的函数。
实验设置
第一阶段:使用计数模式进行工作负载特征分析
IPC 是评估整体工作负载执行效率的第一个指标。
观察结果(表 1):获得的 IPC 值为 0.22,远低于大多数工作负载在 Neoverse N1 上的观测到的数值。例如,像 SPEC CPU(r) 2017(预估)这样的大型基准负载压力测试系统,在该 CPU 上至少能够实现平均 IPC > 1。在 Neoverse N1 上,最大可实现的 IPC 是 4,这通常是运行小型和高度优化的内核而不是大型应用程序得到的。
cycle计数
作为识别工作负载性能瓶颈的第一步,让我们使用与cycle计数相关的事件(表2)来查看使用的cycle分布,按照图6中的方法进行操作。
总体cycle分布百分比(图8)显示,工作负载明显受到后端限制的影响。
观察结果(图8):总cycle的83%在后端停顿,0%在前端停顿,这占用了由流水线停顿浪费的总体执行时间的83%。
由于工作负载受到后端限制的影响很大,我们现在将逐个查看后端事件。首先,我们将查看缓存性能和指令类型组成情况,以确定工作负载是CPU核限制还是内存限制。我们已经从循环指针追逐代码中得到了一个提示,该代码实质上在此情况下执行对CPU高速缓存的访问,这表明可能会受到内存限制的影响。
接下来,我们将查看缓存性能和指令混合情况,因为它们可以揭示应用程序是否受CPU核限制或者是否存在缓存性能问题。
数据缓存(D-Cache)效率
数据缓存(D-Cache)效率事件(图9)显示了所有数据缓存层次中的cache miss,其中最后一级缓存的读miss数相对总读取次数更显著。这表明该工作负载受到内存限制的影响。
顺便提一下,由于L2是共享的缓存,用于L1 D-Cache和L1-I Cache,因此L2和最后一级缓存的refill也可能由于前端指令miss而引起。在评估缓存性能时,始终检查L1-I miss数量是有意义的。但是,对于这个工作负载,由于前端停顿可以忽略不计,我们不认为有很多L1-I miss。
下表3列出了一些详细的缓存效率指标:
观察结果(表3):L1 D-Cache MPKI非常高,为106,其中53%的L1 D-Cache访问导致了refill。指令高速缓存记录显示没有L1-I miss指令,L1 I-Cache MPKI和miss率都很低,符合预期。L2 Cache MPKI也很高,为78,其中53%的访问需要refill。此外,最后一级缓存读取MPKI显示对我们的内存带宽资源施加了非常强烈的压力,因为99%的读取请求无法满足,需要回到主存储器进行LL请求。没有L1-I miss,L2和最后一级缓存的压力仅来自后端内存系统。
接下来让我们看一下指令组成,以查看正在执行的内存指令百分比
观测记录 (图 10):指令比例显示60%为整数运算,20%为load指令和20%为跳转指令。由于我们在工作负载中看到了显著的缓存miss,这告诉我们该工作负载存在内存瓶颈,需要优化以改善其缓存压力,以提高性能。虽然该工作负载不受前端约束,但仍值得检查分支效率计数,因为该工作负载也包括20%的分支指令。
分支效率
为了评估分支预测的效率,以下在表5中导出了错误预测率和MPKI指标:
观察结果(图11):该工作负载的分支误预测率微不足道。否则,我们将会看到前端阻塞,这是我们所期望的
由于我们已经确定了工作负载是内存受限的,因此让我们也看看TLB的性能如何。由于我们几乎没有遇到前端停顿,因此我们不希望出现L1I TLB性能问题。
TLB效率
为了评估TLB的效率,我们在表6中得出了table walk MPKI指标:
观察结果(表6,图12):指令端TLB miss 可以忽略不计,而数据端TLB显示显著的 table walk计数。这表明,工作负载确实会导致一些内存访问产生数据端page
miss,从而进行页表查找。
工作负载特征总结
图13和14展示了所有MPKI和miss率的总结在一个图表上。
工作负载执行重点总结
在N1SDP平台上,stride微基准测试的IPC非常低,仅为0.22。从特征数据来看,工作负载受到后端限制,后端停顿率非常高,达到83%。工作负载有60%整数,20%分支和20% laod操作。工作负载在数据缓存方面表现出显著的后端压力,L1D MPKI为106,L2 MPKI为78,最后一级缓存读MPKI为195。CPU的前端操作非常平稳,诸如Branch MPKI,L1I MPKI和ITLB MPKI等统计数据都很小,对应着0%前端停顿。特征数据支持工作负载行为(如测试源代码注释中所述),即我们的系统存在内存瓶颈,因此我们应该下一步研究如何解决这个问题。
用于分析热点的事件
为了优化代码,我们需要找到代码执行瓶颈,我们应该进一步深入研究两个方面:后端停顿和分层D-Cache事件。除了INST\_RETIRED和CPU\_CYCLES之外,热点分析的短列表事件包括:
- L1D\_CACHE
- L1D\_CACHE\_REFILL
- L2D\_CACHE
- L2D\_CACHE\_REFILL
- LL\_CACHE\_RD
- LL\_CACHE\_MISS\_RD
第二阶段:使用Perf事件采样模式的热点分析
我们收集了所有热点分析的事件的Perf样本数据,并研究了注释的反汇编代码。
Stride基准测试源代码
观察结果(图16):采样cycle和指令显示99%的样本是ldrb指令,即通过指针追逐访问数组元素的load指令。指令和缓存miss事件的热点代码区域也来自同一指令,99%的样本也在那里采样,采样于load指令后的“subs”指令,该指令通过指针追逐访问数组元素。图16中的注释汇编代码显示了“ldrb”和“subs”指令的高亮显示。请注意,性能采样可能会引入漂移,因此热点代码行在load之后的subs指令上,这是瓶颈所在。这表明,该应用程序的主要瓶颈归结为数组访问的性能问题。
第三阶段:代码优化
减轻内存压力的一个众所周知的优化方法是预取,可以通过硬件或软件来实现。在这种情况下,算法每隔七个cache line追逐指针,并且通过仔细观察,可以清楚地看到stride具有一定的模式。我们尝试了在工作负载上进行软件prefetch,并获得了更好的性能,这表明在测试平台上,硬件预取器对此模式不太有效。预取代码位于图15中的源代码中(绿色高亮显示)。在循环内添加了一个预处理指令,启用了软件prefetch并调整预取距离。
对于\_\_builtin\_prefetch调整,我们使用选项(0,0)进行读取访问调整,以预取到L1数据缓存中,因为我们只load数组元素一次,并且数据不会被重复使用和存储在其他层次结构中。随后,我们还针对多个预取距离值对软件prefetch进行了训练,直到在DIST = 40处获得了最好高性能,如图17所示。
优化后代码特征总结
通过软件预取,我们实现了2倍的性能提升-执行时间从16秒减少到了8秒。现在让我们利用PMU事件看一下优化的代码如何提升性能,并将其与未优化的代码进行比较。我们首先看一下IPC的变化。
观察结果(表7):应用优化后,我们将cycle减少了一半,与获得的2倍性能提升相匹配。我们还有了大约3倍的IPC改进,改变了193%。请注意,预取代码的额外增加还使retried的总指令数量增加了39.7%。让我们比较优化后代码和基准代码的反汇编,看一下指令的增加情况。基准和优化后代码之间的差异如下:
让我们看看指令组成是否反映了表7中的这些变化。
观察结果(表8):正如预期的那样,load操作加倍了,因为软件预load指令(prfm)计入了LD\_SPEC事件。其他指令被DP\_SPEC事件计数,增加了33%。
由于主要的性能瓶颈是内存压力,让我们看看缓存效率指标的改善情况。由于软件预取指令被计数为一次load操作,因此MPKI不能作为两次运行之间的公平比较,因此我们将仅查看如表9所示的miss率和访问计数的变化。
观察结果(表9):由于优化代码执行两倍的load操作,因此L1 D-Cache访问次数增加了一倍。由于在读模式下预取到L1,因此我们成功地将L1 D-Cache miss率显著减少了68%,这是导致获得2倍性能提升的原因。
L2缓存访问和LL缓存读取访问保持不变,因为我们没有预取任何这些层次结构。
最终总结
这个案例研究演示了如何使用Neoverse N1 CPU核的硬件PMU或基于选择的PMU事件的工作负载特性的方法学[第2章]和利用[第3章]的方法。虽然这个例子相当简单,但它演示了一种用于剖析性能瓶颈原因,解决问题并验证优化是有效的方法。这个基本流程可以应用于更复杂的工作负载的分析性能问题。
鸣谢
感谢以下人员帮助指导和校对此文档
Rodney Schmidt, Andrea Pellegrini, Frederic Piry, Michael Williams,
Al Grant, James Greenhalgh, Mike Schinzler, Mark Rutland and Tamar Christina