高效可靠的处理器微体系结构性能测量技术
内容分析:
1. 关于Core PMU的工作
2. ARM架构下的的内存带宽质量问题
今天分享的主题主要侧重于处理器微体系结构的性能测量。先简单介绍我们实验室的工作。
主要做数据驱动和系统优化。整个系统优化的过程是一个三步走的闭环,从可靠的性能开始通过一些工作负载分析,包括一些 Monitoring、Profiling、Tracing 手段收集性能数据,再通过一些具有可解性的性能分性能分析方法,包括 Iron law、TMAM、CPI Breakdown 分析方法得到性能瓶颈。再针对性能瓶颈做一些特定的优化,包括编译器层面的一些优化,以及二进制翻译以及 PGO/LTO 方面的一些优化。这些是我们实验室整体的方向。
如果要对软硬件全栈做一个全量的性能分析,从上层的工作负载开始,包括一些 Benchmark、Real-life Application,然后到系统软件各类资源的利用率,再到底层的硬件。要做整个全量分析需要考虑一个很关键的问题就是如何去适应多样化的硬件。可以看到数据中心中,可能有不同型号的不同代机的不同架构的处理器。可能会同时存在于数据中心内,所以怎么去应对硬件碎片化的问题是我们需要特别关注的一个问题。
今天分享的主要有以下几个方面。如果使用架构处理,主要还是在PMU 中。PMU 分为 Core PMU 和 Uncore PMU。Core PMU 主要是在处理器和内部的一些 PMU。Core PMU 是一些多个和直接共享的一些组件,包括 System Level Cache 以及 Memory Controller 和I/O Device 相关的一些组件,上面也会有一些 PMU。今天我分享两个工作,一个是关于 Core PMU 的工作,怎么样才能更高效的复用这些硬件性能计数器,针对 Core PMU,主要解决问题是解决不同架构之间处理器的碎片化问题。然后,第二个工作是 ARM 架构下的内存带宽质量问题。在 ARM 架构上去看设备碎片化问题。
01.关于 Core PMU 的工作
先介绍 Core PMU 的高效复用。
Redis 跑在 Arm 架构上有如图的性能报告,有很多指标包括指令集流水线、Cache、Memory access、Instruction Mix 方面的一些性能指标。
这些性能指标都是通过 PMU 采的一些性能事件,通过性能事件的计数值去导出这些性能指标。图上列了一些性能指标如何来的。
PMU 面临一个最关键的问题是要测事件太多但是计数器很少。上图是一个 Arm v8的结构,分为两种性能计数器。下面六个叫通用型的计数器,可以通过旁边的控制寄存器去决定这些寄存器监测哪一种事件。上面的是专用计数器,只能监测一个特定的性能事件。主要有两种。但是大体上由于计数器本身是处理器内部的寄存器,所以数量不会很多,与上一张图上的需要采的性能事件相比还是太少。
通常做法是做复用,做分组,在不同的时间片让这些事件调度到计数器上做测量。可以在一次测量中拿到所有需要测量的事件。上图例如有十个时间要测,但是只有四个计数器可以用,可以把十个计数器分成三组,每组各占用三分之一的时间去做测量。
复用实际上还是需要用户态时间的测量工具与操作系统内核,包括底层的硬件性能计数器共同工作。最上一层是用户态的测量工具,会根据需要测量的指标去定义事件组,将事件组送到操作系统的内核中Perf_Event Subsystem 负责,Perf_Event Subsystem 会负责不同事件组的调度,不同时间内把事件组放到计数器上做测量。该流程需要硬件以及操作系统内核以及底层的工具进行工作。在复用过程中因为有些事件并不是全程在做测量,所以拿到事件的计数值,并不代表整个程序的运行期间真实发生的事件数量,所以接着需要做一个估计。例如每个事件只采了三分之一的事件,会将原始的计数值除以三分之一放大得到一个估计值。三分之一称为采样比,采样比越高,对于得到的数据更加准确。
现在的工具存在一些问题。一些工具的实践 LIKWID(x86上的性能测量工具)、Arm Top-down Tool(Arm 官方提供的开源的测量工具),事件组定义包含的事件数量不多。事件组只包含2-4个事件,数量低于可用的性能计数器数量。如果将这些事件组送入 Predefined event 中做调度,可能会出现这样情况:对计数器利用率不高,有些计数器会空洞,没有利用到。第二是整个调度器会产生不公平的现象,第一个事件组在一个周期中被调用1或两次,但是第二个第三个事件组只被调用一次。
上图列出了一些工具的事件组。包含的数量是两个到四个。
第二个问题是操作系统在做复用计数器的调度时,调度的策略存在一些问题。如果将八个事件送入 Perf_Event 中做调度,最后的采样比数据如图。前三个事件使用专用计数器做测量,但是采样率不是百分之一百。第二个是45678事件使用通用计数器做测量,但是第八个事件采样率远远低于其它同样使用通用计数器的事件。第三个是8个事件进入后,计数器的利用率反而下降。7个事件时计数器都可以百分百利用,但是如果放8个事件,平均用到6.25个性能计数器。所以 Linux 内核对事件复用的调度计数器存在一些缺陷。
第三个问题是处理器内部到底有多少个性能计数器不容易得知。首先x86上如果将 NMI Watchdog 进程打开,会占用一个通用性能计数器,所以并不是所以存在的性能计数器都可以用来测量。第二是 Arm 架构的处理器,厂商可以做一些定制化操作,实际上处理器到底做了多少个计数器与架构的定义存在偏差。例如 Kunpeng920基于 Armv8 实现,Armv8 官方文档中写明每个核上的计数器是6个,但是 Kunpeng920 实现了12个。所以到底有多少计数器可用值得探讨。
上述讲了 Core PMU 上复用的三个问题。
第一个是到底有多少计数器可用,第二是 Perf_event 调用机制的问题,第三是上层用户态的事件分组效率高不高效。针对这三个问题形成了一套解决方案:在 Arm 和x86上都适用的一套框架。
第一个方案是到底有多少计数器可用。
如果想知道有多少性能计数器可用,只要找到能被同时测量的事件最大值。例如一个事件放入是100%,两个事件也是100%,到5个事件时每个事件发生复用,每个事件只有80%能够用于测量。复用触发后可以得知现在有4个计数器可用,是一种压力测试的思路。
但是事件之间存在一些性能约束。有的事件不能放在一起采,所以需要考虑这方面因素。在每一步加的过程中需要做一些检测。查看新加的事件是否和原有的事件产生冲突,保证在加的过程中可以顺利进行。通过这样的方法找到 Kunpeng920上有12个计数器可以用。这是关于计数器探测的问题。
下一个是 Linux 内核如何调用性能事件。
右侧是算法描述,左侧是八个事件在操作系统中调用的示意图。调度分为两层,一个是平台无关层调度,一个是平台相关层调度。平台无关层调度会将事件顺序调换,让每个事件用到底层计数器。平台相关层将事件分配到相关计数器上。第一行最开始8个事件放入后,在平台无关层按照顺序一个个向相关层送性能事件。平台相关层将事件塞入某个计数器上。到第八个事件送到平台相关层分配计数器时,会发现所有计数器已经用满。第八个事件无法塞入,就会向平台无关层通知分配已经失败,调度结束。
在第二轮调度时就会出现问题,第二轮调度时会更改每一轮事件的顺序,将第一个事件挪到最后,事件就会变为23456781,按照顺序逐个往相关层送性能事件,到第八个事件后会产生问题。第八个事件需要通用型计数器,但此时通用型计数器都被占满,所以第八个事件还是不能塞入。该轮调动失败,不会再对后面事件进行分配,调度结束。会导致在该轮调度中,计数器会空,345依次往后,导致第八个事件的采样率远远低于其他事件。这就是调度内核缺陷的成因。概括就是操作系统不会使用专用计数器做事件的测量。
这是用户态的解决方案。既然操作系统内核对于使用专用计数器的性能事件处理不够好,就不让使用专用计数器的性能事件参与调度。在用户态固定住这些事件,不让这些事件送到内核做调度。
最后是如何才能做比较高效的事件分组。如果已经探测到可以有多少计数器可用,那么我们希望每个事件组尽可能包含的事件数量尽可能接近可用计数器的数量,这样在每一轮调度过程中这些事件组可用尽可能使用满计数器。通过这种思路想到一个算法,既遵循用于测量同一个性能事件在一个事件组中的约束,又尽可能合并这些事件组,让每个事件组尽包含的事件数量尽可能接近可用计数器的数量。
具体做法如图,优化效果使用 Arm Top-Down Tool 定义的事件分组进行一些优化。可用看到这么多指标只需要三个分组就可用进行测量。这样定义分组可以有效提高事件采样率。
在复用计数器做测量的情况下,事件采样率在 Arm 和 LIKWID 上都有50%的提高,极大改善计数器的数量。
以上是关于 Core PMU 复用的问题。
02.ARM 架构下的的内存带宽质量问题
第二部分是内存带宽测量的问题。内存带宽是一个重要的性能指标。
CPU 和内存之间存在一些差距。大模型模型的数据量很大,内存带宽会成为性能瓶颈。做工作负载分析时也需要看内存带宽。
但是在Arm上是否容易做呢?先来看x6。Intel 上有一份详细文档UnCore Performance Monitoring,介绍了所有 UnCore 组件的 PMU 的情况以及 PMU 上每个性能事件的情况,每个事件都有详细的说明。例如做内存带宽时,可用在文档中找到 CAS_COUNT.WR event获取内存控制器读和写的次数。CAS_COUNT.RD 中可以看到64bit的读或写,用这个计数值乘 Time 可以得到读写的带宽。
操作系统对于 Intel 上 Uncore 设备的支持比较好,例如现在有一个 Cas Cache 处理器在内核4.5后就有默认的支持,在5.7后对 Imc Cache 也支持。内核直接升到对应版本后就可以在操作系统的设备数下看到 Uncore_Imc 的设备,对应的是控制器。设备中会暴露一些定义好的性能事件,如图可以看到 Cas_Count_Read 和Cas_Count_Write。之后在 Perf 中调用两个事件可以很轻松测出带宽,也可以得知是 Socket0 或 Socket1 上的带宽,区分读写。最终根据数据计算带宽。
在 Intel 上并不复杂,所以在 Arm 上也使用该想法。
在 Arm 上的 Memory Controller 中找到定义的性能事件,查看官方文档使用哪些进行测量。第一是如何访问到 Arm 下的内存控制器。此处使用了一个 Ampere Altra 控制器。
首先在 Ampere Altra 下将内核升到最新版本,发现内核设备数量还是没有内存控制器的设备,然后查看了内核编译的参数,发现是需要打开 CONFIG_ARM_DMC620_PMU Kernel 内核选项,才能重新编到内核才能看到 Arm_DMC620 设备。点开设备可以看到定义了很多性能事件。此处会出现无法知道使用哪个事件做测量,Arm 官方 DMC 文档中也没有提及。下面是可以测量的公式:使用 Clkdiv2_rdwr 事件。在该事件后增加两个字段来区分读写。并且需要在每个内存控制器上对事件进行采样,对采样时间做求和再乘64,最后得到读写的带宽。
但是这样会很复杂。
首先 Arm 架构的芯片的内存控制器很多样, Ampere Altra 使用的是 DMC620 ,但是 Yitian710、Amazon Gravition2 等机器寄存控制器都是不同的。如果想要测量这些芯片的带宽是否需要每个处理器都单独提供一套方案,这是非常复杂的。此外如果每台机器都提供一套方案,也会遇到以下几种阻碍:首先内核是否有驱动的支持,使用哪个性能事件。即便知道使用哪个性能事件,如何才能使用性能事件导出带宽的数据。
虽然内存控制器使用的都不同,但是有一定数量的芯片使用 Arm 公版的 CMN 线上网络,例如 Ampere Altra、Yitian、Gravition2。所以使用 Arm 公版的线上网络机制来提出一套通用化的内存带宽通用方法是可行的。
线上网络的结构可以理解为一个芯片是一整个 Mesh 形状,右侧中间的框为XP节点,可以用来放不同的设备,例如 Core、内存控制器或与I/O相关的设备等。XP 节点本身有 PMU Controller,可以监控XP 节点连接的设备的流量。所以如果可以找到内存控制器的 XP 节点,通过对应的 XP 节点的PMU去监控和内存控制器相连的流量,就可以用另一种方式拿到内存带宽。这就是以一个通用的方式测量内存带宽。但这种也存在问题:如何知道是哪一个 XP 节点连接的是Memory Controller,这是一个需要讨论的问题。
微架构性能测量本身需要底层硬件、用户测量工具以及操作系统内核去共同完成,可靠性能数据关系到后续的性能分析。本次没有提到的一些细节可以在上图中的一些论文中查看,这些已经发布的论文中讲解 Core PMU 如何进行复用以及一套性能分析工具,能够朝着跨平台的性能测量和性能分析去努力。