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

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

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

 

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

 

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

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

  1. 两个线程,分别为生产者、消费者,模拟 route-worker 模型;
  2. 三个线程,分别为生产者、转发者、消费者,模拟 pipeline 模型。

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

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

奇怪的现象

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

 

 

 

 

绑核说明:

  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

 

 

 

 

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

  • 避免跨 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 的通信。

 

 

 

 

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

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

 

 

 

 

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

 

 

结论与优化建议

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

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

 

 

 

 

可以看出:#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 来自 AMD的官方文档:

 

 

 

 

 

 

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

 

 


 

责任编辑:武晓燕 来源: 腾讯技术工程
相关推荐

2019-03-14 15:38:19

ReactJavascript前端

2023-12-12 17:44:13

三级缓存Bean

2023-11-01 11:59:13

2016-11-23 13:50:29

三级列表页持续架构前端优化

2009-06-12 09:00:15

Linux域名访问

2022-12-02 12:01:30

Spring缓存生命周期

2022-03-01 18:03:06

Spring缓存循环依赖

2022-05-08 19:23:28

Spring循环依赖

2023-11-01 11:51:08

Linux性能优化

2019-03-01 11:03:22

Lustre高性能计算

2022-03-21 14:13:22

Go语言编程

2021-10-13 07:39:05

Windows 11操作系统AMD

2019-04-08 10:09:04

CPU缓存高性能

2023-02-26 11:15:42

缓存循环依赖

2024-03-04 08:47:17

Spring框架AOP

2012-07-10 10:27:58

2012-06-29 15:01:46

2021-08-13 09:06:52

Go高性能优化

2009-01-05 10:00:11

JSP优化Servlet性能优化

2021-05-27 10:02:57

Go缓存数据
点赞
收藏

51CTO技术栈公众号