高性能编程:三级缓存(LLC)访问优化

Posted 腾讯技术工程

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了高性能编程:三级缓存(LLC)访问优化相关的知识,希望对你有一定的参考价值。




AMD 服务器,多线程应用绑核,选取不同的 CPU 核,性能差距可达50%。

最近有幸因项目拿到一台 AMD EPYC 系列测试服务器,发现了一些奇怪的现象。

这台测试服务器拥有双路 AMD EPYC  7552  处理器,属于第二代 Rome(Zen2)架构,单路 48 个物理核,双路总计 192 个逻辑核(线程),有两个 NUMA 节点。

为了进行测试,预先编写了一个简单的多线程程序:

  1. 两个线程,分别为生产者、消费者,模拟 route-worker 模型;

  2. 三个线程,分别为生产者、转发者、消费者,模拟 pipeline 模型。

线程间采用无锁队列通信。生产者依次写入 1 ~100000000,消费者取出数字求和。线程每次写入或读取队列数据后执行一些无意义循环用于消耗时间,模拟业务逻辑。

所有线程分别绑核,避免线程迁移导致 Cache 抖动,且绑定的核心属于同一个 CPU。所有队列均在这个 CPU 的本地内存上进行分配,避免跨 NUMA 的远程内存访问。

奇怪的现象

测试发现,线程绑到不同的核上,有显著的性能差异:

高性能编程:三级缓存(LLC)访问优化
高性能编程:三级缓存(LLC)访问优化

绑核说明:

  1. 核 #4 #5 #6 #8 #12 #100 均为同一个 CPU,不存在跨 NUMA 访问内存的情况;

  2. 核 #4 #100 是一对 SMT 核心,即同一个物理核虚拟出来的两个逻辑核;

  3. 黄条涉及的核 #48 属于另一个 CPU,存在跨 NUMA 访问内存的情况,仅供对比。

测试结果反映了一个很奇怪的现象:线程绑核,在同一个 NUMA 选取不同的核心,性能差距竟然达到 50%(route-worker 模型 #4#5 vs #4#8)甚至 140%(pipeline 模型 #4#5#6 vs #4#8#12)。

这究竟是为什么呢?


复杂的内存层次模型

这要从内存层次说起。通常,根据延迟时间从小到大,内存层次可以划分为:(1)L1,一级缓存;(2)L2,二级缓存;(3)L3,又叫 LLC,三级缓存;(4)内存。

在具体实现上,传统的 Intel 至强系列模型比较简单:

  • 每个物理核虚拟出两个逻辑核(TR1/TR2,TR3/TR4)

  • 每个物理核独有 L1 和 L2

  • 所有物理核共享 L3

高性能编程:三级缓存(LLC)访问优化

这就解释了一些高性能程序开发的优化策略:

  • 避免跨 NUMA 的远程内存访问,除了降低访问延迟,对 L3 也更友好

  • 将线程绑核,避免 Cache 抖动,具体是避免 L1 和 L2 的抖动

  • 共享 L3 的存在是透明的,软件上不关心,也无法关心

这一切,在 AMD 的体系结构中发生了变化。

AMD 于 2017 年发布了 Zen 架构,其中一个重要的设计原则是:一块 CPU 由多个 CCX(CPU Complex)堆叠而成。那么,CCX 是什么呢?简单来说,CCX 实际上就是 4 个物理核(8 个逻辑核)+ L3。CCX 通过 IF 总线与 IO Die 连接(Rome),实现 CCX 间互通以及与内存、IO 的通信。

高性能编程:三级缓存(LLC)访问优化

图片来源:https://frankdenneman.nl/2019/10/14/amd-epyc-naples-vs-rome-and-vsphere-cpu-scheduler-updates/

所以,AMD EPYC 的内存模型就和传统模型有了很大区别:L3 并不由所有物理核共享,而是由同一个 CCX 内的 4 个物理核共享。与 NUMA 引入的“远程内存”概念类似,CCX 引入了“远程 L3”的概念。

高性能编程:三级缓存(LLC)访问优化

在找到一个访问延迟表,供参考:

事件 延迟
一个 CPU 周期(2.3GHz 主频) 0.4 ns
访问 L1 1.6 ns
访问 L2 4.8 ns
访问 L3 15.2 ns
访问远程 L3 63 ns
访问本地内存 75 ns
访问远程内存 130 ns


结论与优化建议

结论是,在 AMD 服务器下,如果要获得更高的性能,要针对 L3 进行优化,方法为:把一组任务(线程、进程)绑定到同一个 CCX 下的核心。

那怎样才能知道哪些核心是同一个 CCX 呢?可以使用 hwloc-ls 命令:

高性能编程:三级缓存(LLC)访问优化

可以看出:#0 #96 #1 #97 #2 #98 #3 #99 是 4 个物理核 8 个逻辑核,它们共享了 16 MB 的 L3,所以这几个核属于同一个 CCX。

因此,绑核的时候,可以绑 #0 #1 #2 #3 #96 #97 #98 #99,又或者 #4 #5 #6 #7 #100 #101 #102 #103,以此类推。

文章开头的测试结果就很好解释了:#4 #5 #6 是同一个 CCX,因为它们共享 L3,每次读写队列其实都是读写 L3,所以性能高;#4 #8 #12 分属 3 个不同的 CCX,每次写队列,都会使得其它 CCX 的 L3 数据失效,导致读队列时必须要从内存中读取,所以性能差。

最后,可以通过:

perf stat -e r510143,r510243,r510843,r511043,r514043 ./xxx 查看 L3 的访问情况,PMC Code 来自 :

高性能编程:三级缓存(LLC)访问优化

高性能编程:三级缓存(LLC)访问优化

可以看到绑核 #4 #8 读取内存次数几乎是绑核 #4 #5 的 3 倍。


以上是关于高性能编程:三级缓存(LLC)访问优化的主要内容,如果未能解决你的问题,请参考以下文章

Java性能优化推荐书!十年Java编程开发生涯

前端性能优化

异步编程CompletableFuture实现高性能系统优化之请求合并

Quora的Python异步编程实践

性能优化之数据存储&DOM编程

DPDK — 性能优化手段