关注了就能看到更多这么棒的文章哦~
Cache awareness for the CPU scheduler
By Jonathan Corbet
April 29, 2025
Gemini-1.5-flash translation
https://lwn.net/Articles/1018334/
内核的 CPU 调度器必须平衡各种各样的目标。系统中所有任务的调度必须是公平的,任何给定任务的延迟都要控制在一定范围内。如果系统中有足够的工作要做,那么系统中的所有 CPU 都应该保持繁忙,但不需要的 CPU 应该关闭以降低功耗。任务也应该在最有可能缓存该任务正在使用的内存的 CPU 上运行。来自 Chen Yu 的这个补丁系列旨在改进调度器处理多线程进程的缓存局部性的方式。
RAM(随机存取存储器)速度很快,但仍然无法以 CPU 能够消耗数据的速度提供数据。因此,系统构建了多层缓存,旨在保存频繁使用的数据并使其更快地可用。从缓存中读取一个值相对较快;而如果读取需要一直走到 RAM 才能获取到的值,可能会使 CPU 暂停执行数百条指令的时间。因此,有效地利用缓存对于应用程序的良好性能至关重要。编写良好的应用程序在实现时会考虑到缓存行为,但内核也需要发挥作用。
每一层缓存都可以被不同数量的 CPU 访问;最接近的(L1)缓存可能特定于单个 CPU,而随后的(较慢但通常更大的)缓存层将由一组 CPU 共享。最后一级缓存(LLC)(Last-Level Cache),离 CPU 最远,因此也是最慢的,但它往往是最大的,并且被最大数量的 CPU 共享。将一个任务从一个 CPU 移动到另一个 CPU 可能会使其远离它在缓存中建立的数据,从而损害其性能。如果一个任务被移动到同一插槽(socket)中的另一个 CPU,那么它的大部分缓存数据可能仍然可以在较低级别的缓存中找到;如果它被移动到另一个 NUMA(Non-Uniform Memory Access,非一致性内存访问)节点,它可能必须从一个空的(从它的角度来看)缓存重新开始。
因为移动一个任务会损害其性能,所以 CPU 调度器会尽量避免这样做。这个目标经常与其他目标相冲突,比如需要在整个系统中平衡负载,以最大限度地利用可用的 CPU 资源。然而,调度器目前没有做的是尝试识别可能共享资源的任务组,也就是说,如果它们一起调度,可能会因为共享同一个缓存而有性能优势。相反,将这些任务分散在整个系统中可能会导致竞争,因为它们会争夺将数据保存在其本地缓存中。
今年 3 月,Peter Zijlstra 发布了一个 RFC 补丁,以探索改善这种情况。它基于这样的想法:如果一个进程有多个线程,那么这些线程很可能共享内存,并且可以从在同一个缓存域中运行中获益。它向描述地址空间的(原本已经很大的)mm_struct
结构体添加了一些检测代码,包括一个 per-CPU 数组,调度器使用该数组来跟踪使用该 mm_struct
的线程在系统中每个 CPU 上花费的时间。这个数据会随着时间的推移而衰减,因此最近的使用情况比遥远、被遗忘的过去(比如几十毫秒前)的使用情况更能被体现。
当唤醒一个一直在等待某个事件的线程时,调度器会访问这个 per-CPU 数组,并确定哪个 CPU 花费了最多的时间来执行来自同一进程的线程。如果感兴趣的线程一直在其他地方运行,它将被移动到所选的 CPU,在那里它将更接近其他线程,并且如果运气好,可以从与它们共享缓存空间中获益。正如 Zijlstra 当时指出的那样:“这个补丁不是为了合并,而是为了测试和开发。我们需要首先让它真正改善工作负载”。
然后,Chen 接手了这项工作,并对其进行了一些改进。最初的代码会将任务移动到“热”CPU,即使该任务已经在同一个 LLC 域中运行,因此已经与该 CPU 共享最大的缓存。在这种情况下,移动只会减慢任务的速度,而不会带来太多(如果有的话)的性能提升,因此在这种情况下会抑制任务迁移。
出现的另一个问题从对原始补丁工作方式的描述中可能看起来很明显。线程被迁移到“热”CPU,而没有考虑到该 CPU 已经有多忙。如果线程数量很大,那么这种聚集很可能会使目标缓存域过载,从而损害整体性能。这个问题通过查看调度器的负载均衡算法生成的总体使用统计数据来解决,以确保拥有相关线程的进程不会使目标域过载。具体来说,如果进程在该域中使用超过 25% 的 CPU 时间,或者在该域中拥有超过 33% 的总体负载,那么调度器将不会将更多线程移动到该域。
这已经改善了情况,但这项工作仍处于相对早期的阶段。例如,它可能会与负载均衡器发生冲突:
任务的聚集将使任务在唤醒期间非常迅速地向首选的 LLC 移动。但是,负载均衡往往会将任务从聚集的 LLC 移开。这两种迁移方向相反,并且往往会在 LLC 之间反弹任务。
CPU 调度是由许多启发式方法驱动的,这些方法经常会发生冲突。因此,添加另一个启发式方法(“将进程的线程集中在单个缓存域中”)的补丁系列肯定会带来更多令人惊讶的交互,这些交互可能需要用更多的启发式方法来解决。
这里还有一个尚未被提出的问题:收集进程的线程是识别将从共享缓存中受益的任务的最佳方法吗?可能不是,但它的优势在于实际上是可能的;检测没有这种直接关系的任务中的缓存共享可能是困难的,如果它可能的话。最终,这个补丁系列的命运将取决于它是否真正显示出对实际工作负载的改进,而不会导致其他工作负载的退步。对于像这样的更改来说,这是一个很高的标准。内核在未来很可能会有更多缓存感知的调度,但似乎还需要一段时间才能准备好。
全文完
LWN 文章遵循 CC BY-SA 4.0 许可协议。
欢迎分享、转载及基于现有协议再创作~
长按下面二维码关注,关注 LWN 深度文章以及开源社区的各种新近言论~