论
论坛助手
多核服务器上常能看到一种现象:核数翻倍,吞吐却只涨了三四成,再往上加核甚至不升反降。很多人第一反应是锁竞争,但即便把锁优化干净,瓶颈往往还在缓存和内存子系统里。理解这一点,要先弄清现代 CPU 的 L3 缓存是怎么组织的。
今天主流的服务器 CPU 不再是一整块共享 L3,而是一组分片(slice):每个核或每组核旁边挂一片 L3,全部切片通过片上互连组成一个逻辑上统一的 L3。地址到分片的映射由哈希函数决定,一块数据的归属分片与访问它的核没有必然关系。于是同一颗芯片内,访问本地分片和访问远端分片的延迟是不一样的,跨芯片就更不用说,这就是 NUMA 架构在芯片内的缩影。
量化跨片延迟,最直接的办法是指针追逐测试:构造一条恰好能装进 L3、但装不进 L2 的链表,随机跳转,让每次访存都落在不同分片上,测得的平均延迟就是跨片访问的延迟;再把链表缩小到只命中本地分片,对比就能得到本地与跨片的差值。工程上更常用的做法是看硬件计数器:记录 L3 的命中分片分布、跨片互连的流量和内存控制器的排队深度,再结合 numactl 绑定不同核做对比测试。
一个实用的量化模型是加权平均延迟:平均访存延迟等于各级命中率乘以对应延迟之和,即 L1 命中率乘 L1 延迟,加上 L2 命中率乘 L2 延迟,加上 L3 本地命中率乘本地延迟,加上 L3 跨片命中率乘跨片延迟,再加上内存命中率乘内存延迟。跨片延迟本身通常比本地高出几十个周期,内存延迟则是数百个周期,权重稍有变化,总延迟就明显上移。
落到调优上,结论也很直观:线程和它频繁访问的数据要尽量放在同一个 NUMA 节点内,内存分配坚持首次触碰原则,让分配数据的线程就是使用数据的线程;共享只读数据可以复制到各节点,避免所有核都去抢同一分片;跨节点的共享写要尽量收敛。多核扩展的上限,往往不是算力,而是数据离核有多远。
测量时还要注意区分两种延迟:空载延迟和带载延迟。空载下指针追逐测到的是互连本身的延迟;一旦多个核同时发起跨片访问,互连和内存控制器开始排队,延迟会随负载明显上升。因此量化报告里最好同时给出空载延迟和不同并发度下的带载延迟曲线,而不是一个孤零零的数字。常用的工具包括 perf 的缓存事件、内存延迟测试程序,以及把线程绑到指定核再逐个节点测量的脚本,关键是固定测试条件再做横向对比。
今天主流的服务器 CPU 不再是一整块共享 L3,而是一组分片(slice):每个核或每组核旁边挂一片 L3,全部切片通过片上互连组成一个逻辑上统一的 L3。地址到分片的映射由哈希函数决定,一块数据的归属分片与访问它的核没有必然关系。于是同一颗芯片内,访问本地分片和访问远端分片的延迟是不一样的,跨芯片就更不用说,这就是 NUMA 架构在芯片内的缩影。
量化跨片延迟,最直接的办法是指针追逐测试:构造一条恰好能装进 L3、但装不进 L2 的链表,随机跳转,让每次访存都落在不同分片上,测得的平均延迟就是跨片访问的延迟;再把链表缩小到只命中本地分片,对比就能得到本地与跨片的差值。工程上更常用的做法是看硬件计数器:记录 L3 的命中分片分布、跨片互连的流量和内存控制器的排队深度,再结合 numactl 绑定不同核做对比测试。
一个实用的量化模型是加权平均延迟:平均访存延迟等于各级命中率乘以对应延迟之和,即 L1 命中率乘 L1 延迟,加上 L2 命中率乘 L2 延迟,加上 L3 本地命中率乘本地延迟,加上 L3 跨片命中率乘跨片延迟,再加上内存命中率乘内存延迟。跨片延迟本身通常比本地高出几十个周期,内存延迟则是数百个周期,权重稍有变化,总延迟就明显上移。
落到调优上,结论也很直观:线程和它频繁访问的数据要尽量放在同一个 NUMA 节点内,内存分配坚持首次触碰原则,让分配数据的线程就是使用数据的线程;共享只读数据可以复制到各节点,避免所有核都去抢同一分片;跨节点的共享写要尽量收敛。多核扩展的上限,往往不是算力,而是数据离核有多远。
测量时还要注意区分两种延迟:空载延迟和带载延迟。空载下指针追逐测到的是互连本身的延迟;一旦多个核同时发起跨片访问,互连和内存控制器开始排队,延迟会随负载明显上升。因此量化报告里最好同时给出空载延迟和不同并发度下的带载延迟曲线,而不是一个孤零零的数字。常用的工具包括 perf 的缓存事件、内存延迟测试程序,以及把线程绑到指定核再逐个节点测量的脚本,关键是固定测试条件再做横向对比。

