macOS如何高效管理M1 CPU的众多核心?

发表时间: 2022-05-07 11:02

编注:本文 原文 发表于 Howard Oakley 博士的个人博客,文章的翻译、注释以及发布均已得到作者同意。为了补充一些细节以及便于普通读者理解,本文对原文有一定程度的修改。


1984 年 1 月,Apple 开始设计、开发和销售个人电脑系列产品 Macintosh。在这近 40 年时间里,Apple 浓墨重彩地书写了许多科技史上的里程碑,其中就包括了三个非常重要的时间点——1994 年从摩托罗拉 68000 架构迁移至 PowerPC 平台、 2005 年从 PowerPC 平台迁移至英特尔 x86 平台、2020 年从英特尔 x86 平台迁移至 Apple Silicon。

2020 年 11 月 11 日 Apple 在加州 Cupertino 正式发布了 M1 芯片,不仅是 Apple 自己首款基于 ARM 架构的用于个人电脑的自研处理器,而且其强大的性能也让当时苦「牙膏厂」久已的 Geeker 们也感到异常亢奋。

不过,看得见的风光总是与看不到的努力分不开的。作为专为 Mac 设计、优化的芯片,系统到底是怎么将程序调度在 M 系列处理器上的。

关于 Howard Oakley 博士

Howard Oakley 博士目前是一名 Mac 软件的开发者,同时也是网站 Eclectic Light Company 的创始人。他与 Mac 的缘分始于他对 Macintosh SE 和 Macintosh Programmer's Workshop 的一见钟情,从此他沉溺于其中、不可自拔直至今日。

不对称的处理器架构

在以前的 Intel 处理器款的 Mac 中,Intel 处理器所有的核心都是相同的,因此这种处理器是 对称多处理(Symmetric multiprocessing,缩写为 SMP)构架。系统要做的事情其实很简单,保持每一个核心的负载大体相近即可。

在 Intel 处理器款的 Mac 上打开「活动监视器」的 CPU 历史窗口,我们可以注意到图表分为了两列,左半部分奇数核是真实的物理核心,右半部分则是 Intel 超线程技术虚拟出来的核心。可以看到,在高负荷的情况下,系统将负载均匀的分散到了所有的核心上,而负载较轻时系统则将负载主要放在了真实的物理核心上。

横坐标轴为时间,纵坐标轴则为负载,时间从左到右逐渐靠近当时的情况

但 Apple Silicon 上的 CPU 则完全不同,它的处理器部分都是由两种不同的 CPU 核心组成,一种是叫做 Firestorm 高性能核心(有时也被缩写成 P 核),而另一种则叫做 Icestorm 高能效核心(有时也被缩写成 E 核),这种不对称的处理器被称为 非对称多处理(Asymmetric Multiprocessing,缩写为AMP、ASMP)器,或是 异构计算(Heterogeneous Computing)处理器。

从 2020 年开始到今天, M1 系列共计发布了四款芯片,分别是:

  • M1 (2020 年)
  • M1 Pro 与 M1 Max (2021 年)
  • M1 Ultra (2022 年)

E 核心有 5 个频率可以选择,P 核心有 15 个频率可以选择

通过 powermetrics 我们可以知道 E 核心的频率最高为 2064Mhz,P 核心的情况则分为两种情况,M1 芯片的 P 核心最高频率为 3204Mhz,M1 Pro/Max/Ultra 的 P 核心最高频率可达 3228 MHz。如果系统还是和以前一样将保持所有的核心负载相近,不仅会浪费 P 核心更多的中间档位,也会让跑在 E 核心上的程序明显更慢。

此外,M1 和 M1 Pro/Max/Ultra 有完全不同的 E 核心和 P 核心组合,每个处理器还可以选择不同的 CPU 数量,比如最基础款的 14 inch MacBook Pro 上的 M1 Pro CPU 部分只有 6 个性能核和 2 个能效核,所以开发者如果需要手动适配「保持所有的核心负载相近」这个逻辑从直觉上来说异常繁琐。

为了简化核心管理,macOS 会将核心根据功能划分为 2~4 个相同类型的集群,集群可以理解成组。然而,系统层面上的内核编号和 powermetrics 中显示的内核编号相同,但和活动监视器中所显示的内核编号却并不一样;因此为了行文统一,文中将采用活动监视器的内核编号规则,但根据系统集群进行编号。在 macOS Monterey 12.3.1 下, M1 系列的三组芯片的功能集群情况如下:

  • M1 分别由一个 E 集群(包含 4 个 E 核心)和一个 P 集群(包含 4 个 P 核心),并命名为 E 和 P0
  • M1 Pro/Max 则由一个 E 集群(包含 2 个 E 核心)和两个 P 集群(分别包含 4 个 P 核心),并命名为 E、P0 以及 P1
  • M1 Ultra 则由一个 E 集群(包含 4 个 E 核心)和四个 P 集群(分别包含 4 个 P 核心),并命名为 E、P0、P1、P2 以及 P3

从理论上来讲,一个集群内的所有核心都会在相同的频率下运行,并且通常(但不总是)保持每一个集群内「核心的负载」大体相近。极端情况下甚至会发生系统一股脑儿地把所有的任务安排到某集群中的一个核心上。

比如 Logic Pro 导入素材就会出现这种极端情况

线程控制是如何进行的

实际应用开发中,macOS 并不提供公开的 API 让应用程序直接使用具体的核心、核心类型或是集群;相反,应用程序通常由 Grand Central Dispatch 使用 QoS 管理,然后 macOS 会使用这些设置来确定具体线程的管理策略。

在实际情况中,QoS 最低的线程只会派发至 E 核集群,而较高的 QoS 的线程则可能会被派发到 E 或 P 核集群。尽管可以通过命令工具 taskpolicy 或者代码中的函数 setpriority() 对派发进行动态修改,然而它却只对较高的 QoS 线程有效。「最低 QoS 线程只在 E 集群上运行」的规则始终不变。

通过 macOS App Store 安装 Xcode 的线程 QoS 就是最低的,完全不会使用 P 核

macOS 自身的策略是大部分后台任务都以最低的 QoS 运行。这当中包括了 Time Machine 的自动备份、Spotlight 索引更新以及 Archive Utility 的压缩和解压。这当中值得一提的是,对于 Archive Utility 很多人可能会有一个直观的感受:下载了一个 xip 格式的 Xcode 副本,解压的时候需要耗时 N 久,其实这就是很多代码被限制在 E 核上运行所致,而且用户也不能主动将它调往 P 核上运行。

后台线程(Background threads)

因为 M1 和 M1 Pro/Max 芯片上的 E 核集群大小不同,前者有 4 个 E 核,而后者只有 2 个,所以 M1 和 M1 Pro/Max 上的最低 QoS 线程的加载与运行方式是有不同之处的。

在拥有 4 个 E 核的 M1 芯片上运行 QoS 为 9 的线程时,每个 E 核核心频率为 1000M(1 GHz)左右;而在只拥有 2 个 E 核的 M1 Pro/Max 中运行同样 QoS 为 9 的线程时,如果只有 1 个线程那么 E 核的运行频率也同样为 1000 MHz,但如果有两个或者更多,那么每个 E 核频率就会增加到 2064 MHz。这样的设计确保了即使集群大小不同,但 M1 Pro/Max 中 E 集群至少能提供和 M1 相同的后台任务处理性能。

当然这里依然会有例外,像 backupd 这类拥有最低 QoS 的线程,在运行时如果同时受到来自 I/O 的限流,那么即使是在 M1 Pro/Max 上也总会以约 1000MHz 的频率运行。

用户发起的线程(User threads)

所有 QoS 高于 9 的线程处理都大同小异,它们之间的区别无非就是它们的优先级不同。高 QoS 的线程有资格运行在任何一种核心或者集群上,不过 M1 和 M1 Pro/Max 上的处理方式又有所不同。

在 M1 上,由于只有 1 个 P 集群和 1 个 E 集群,而物理核心总计有 8 个,因此同一时刻最多只有 8 个线程可以分配到这两个集群上,每个集群均可以分到 4 个线程。如果同一时刻需要分配到线程数小于等于 4 时,系统会尽量将他们放在 P 集群上运行;除非当前队列中有更多更高 QoS 级别的线程等待运行,这时才会额外使用 E 集群运行这类任务。在上述情况下, P 核心的频率的最大值将为 3GHz,E 核心的频率最大值则为 2GHz,是运行 QoS 为 9 的线程时的两倍。

但 M1 Pro/Max 却有 3 个集群,两个分别拥有 4 个 P 核的集群,以及一个拥有 2 核的 E 集群。如果同一时刻需要分配到线程数小于等于 4 时,系统会主动将线程分为到第一个 P(P0) 集群上,第二个 P 集群将始终保持未加载和不活跃的节能状态;如果同一时刻需要分配的线程多余 4 时,多出来的线程(大于等于 5 个小于等于 8 个)将被分配到第二个 P(P1) 集群上;如果此时还有更多的线程(大于等于 1 个小于等于 2 个)等在运行,那么这些进程将会再分配到 E 集群上。在上述情况下,P 核心的频率的最大值将为 3228MHz,E 核心的频率最大值则为 2064MHz。

M1 Ultra 芯片总共有 5 个集群,每个集群有 4 个核心。它们的策略大体与 M1 Pro/Max 相同,只不过在使用 E 集群之前,优先调用的是 4 个 P 集群。

不过,有两种情况下,代码似乎只在单个核心上运行:

第一种情况发生在引导过程中,在内核初始化并运行在其他核心之前,代码只运行在单个 E 核上。另一种情况则发生在,下载完 macOS 更新以后处于「准备」阶段时,在 M1 Pro/Max 芯片上,macOS 的 5 个更新线程仅仅被赋予了一个 P 核的活动驻留权利,即 2 个 P 集群中的第一个集群中的第一个(P0,下面标为核心 3 )。

在准备安装更新的 30 分钟内,这种不常见的的活动驻留一直都在。

负载模式下的图形(Patterns under load)

这里有几个关于 macOS 策略影响调度的典型例子,这些例子取自活动监视器的 CPU 历史窗口。

上图显示了 M1 芯片的一系列负载的情况,这些负载来自于逐渐增多的 CPU 密集型线程。上面提到过 M1 有两个集群 E 和 P0,这里分别用蓝框进行划分。从左开始,第 1~4 个高优先级的进程的负载全部由 P0 集群承担,而后续第 5~8 个进程的负载则逐步由 E 集群开始承担。

这张图则展示了重负载下 M1 Pro 不断变化的负载情况,其中一些线程是后台进程,而另一部分则是高优先级进程,虽然绝大部分的负载都由 E 集群承担,但 P0 集群也承担了不少负载,而 P1 集群主要被用来处理一些峰值负载。

最后一张图则是 M1 Ultra 上的运行情况,作者本人已将对应的核心重新排列到了对应的集群中,其中 E 位于顶部,P0~P3 从第二排开始从左到右、从上到下依次进行排列。而图中所示的负载则是一个非常典型的情况——系统登录后的前几分钟,可以看到 E 和 P0 承担了绝大多数时候的负载,而且在负载更重的时的初期,系统会将更多的任务调度到 P1~P3 剩下的 3 个 P 集群上,以更快得完成任务。

目前,活动监视器尚未提供一个关于 M 系列处理器的重要信息——集群频率。在 CPU 处于 100% 负载时,此时相当于活动驻留,集群在频率低于 1000MHz 时完成指令的速度不到频率为 2064MHz 的相同集群的一半快。可惜的是,目前唯一可以获得频率信息的手段是命令工具 powermetrics

下图是一份 macOS 对 M1 、 M1 Pro 和 Max 芯片中 CPU 核心的管理概要。目前,关于 M1 Ultra 的信息尚在整理中,并在后续进行增补完善。如果你在用 M1 Ultra,熟悉并且愿意提供帮助,欢迎与作者 Howard Oakley 博士进行联系。

今年 6 月,Apple (可能)会在 WWDC 大会上宣布其 M1 系列的后继者。届时,或将能够看到它们的核心架构以及 macOS 提供的管理策略。

感谢 Walt 提供的关于 Ultra 的信息以及负载下的截图。