深入理解Arm CMN-700:SLC内存系统与缓存分区调优指南
做服务器底层优化的朋友应该对这一两年的 Arm Neoverse 平台不陌生。CMN-700 这个名字你大概率见过但很多人一开始容易把它当成一颗芯片、或者一个单纯的互连开关。直到你真正去碰 SLCSystem Level Cache分区、去排查多核访问延迟抖动的时候才会意识到CMN-700 才是决定整个内存系统行为的关键。它不是缓存本身而是缓存、一致性、内存控制器和各种加速器之间的“交通总调度”。这篇内容我打算把 CMN-700 的 SLC 内存系统和缓存分区技术拆开来讲。如果你正在做 Neoverse 平台上的性能调优或者想搞清楚 Arm 服务器为什么能在大规模并行负载下保持稳定的缓存命中率又或者你在选型评估时被“CMN-700 支持缓存分区”这句话打动过那么这篇文章应该能给你一些从事后视角出发的参考价值。我不打算写成一本地手册而是按我实际接触项目时的思路从设计逻辑讲到配置方法再聊一些坑。1. 先想明白一件事CMN-700 到底是什么1.1 它不是 CPU也不是简单的总线CMN-700 是 Arm 在 Neoverse 服务器生态里推出的互联 IP全名是 Coherent Mesh Network 700。你可以把它理解成一颗“看不见的计算核心”CPU 核心、GPU、PCIe 控制器、内存控制器、IO 设备全部通过 CMN-700 连接在一起。它不是一个简单的总线而是一个由多个节点组成的网格mesh每个节点承担不同的角色比如请求节点 RN、家庭节点 HN、从节点 SN。正是这种 Mesh 结构让 CPU 核心不一定要跑到很远的地方去拿数据而是可以在一个二维网格里按最短路径完成请求。刚开始接触这个概念时我习惯把 CMN-700 类比成一个城市的路网系统。CPU 核心是住在各个小区的居民内存是远处的仓库而 SLC 则是每个街区内的小型便利店。居民去买东西如果便利店有货就不用跑大老远去仓库如果便利店没货再按路网去仓库。CMN-700 的路由规则、缓存管理规则决定了整个城市的物流效率。1.2 CMN-700 在 Neoverse 平台里的实际位置具体到一颗服务器 SoC 里CMN-700 通常位于 DSUDynamIQ Shared Unit背后、内存控制器之前。它可以连接多个 DSU cluster也就是把若干个小核心簇和大核心簇统一接入同一个一致性域。SLC 则挂在 CMN-700 上被所有 cluster 和 IO 设备共享。这个位置很关键因为 SLC 距离内存控制器很近距离 CPU 核心稍远。它缓存的是所有核心能够共同访问的数据包括指令、页表、一致性消息以及来自网卡或加速器的数据。由于它在内存控制器之前只要能命中 SLCCPU 和 IO 设备都可以避免一次实际 DRAM 访问这能显著降低延迟和带宽压力。CMN-700 给我的感觉是它不再像手机 SoC 里那种“专门给 CPU 用的 L3 cache”而是整个 SoC 的一致性互连中枢。这里的 SLC 既为 CPU 服务也为网卡、加速器、虚拟化场景服务。理解这一点才能明白后面为什么要做缓存分区。1.3 从 CMN-600 到 CMN-700最大的变化在哪儿CMN-600 在 Neoverse N1 平台里已经很成熟但它主要面向 32 核心左右的规模SLC 的容量和分区能力相对有限。到了 CMN-700可变参数更多了。比如支持更大规模的 mesh 节点数量、更高的请求带宽SLC 容量也可以通过不同数量的 SRAM bank 组合扩展。更重要的是CMN-700 对“共享缓存分区”的支持更完整能够按 way 进行隔离还提供了更细粒度的 QoS 优先级控制。在我实际测试的项目里同样一个数据库负载从 N1 平台迁移到 Neoverse V1 平台上因为 CMN-700 的 SLC 管理能力和分区配置不同尾延迟表现有明显的差异。这不是单纯 CPU 性能提升带来的很大程度是因为 SLC 对大流量并发请求的吸收能力变强了。2. SLC 内存系统的设计逻辑不是“加一块大缓存”这么简单2.1 先分清楚 L3、SLC 和 LLC 的区别很多人容易把 SLC 和 L3 Cache 混在一起。在 Arm 的经典大小核架构里DSU 内部会有一个共享的 L3 cache这个 L3 只归属于这一个 DSU cluster其他 cluster 访问不了。SLC 则在 CMN-700 里是多个 DSU cluster、IO 设备共同可见的缓存。LLCLast Level Cache是一个更泛的概念指系统中最后一个软件可见的缓存层级。在 Neoverse 多 cluster 场景下LLC 往往就是 SLC而不是某个 cluster 内部的 L3。这里有一个很微妙的问题如果是单 cluster 的小 SoCDSU 内部的 L3 可能就是 LLC但如果是多 cluster 服务器L3 只是每个 cluster 私有的一层过滤真正的最后一级缓存是 CMN-700 的 SLC。所以做性能分析时看 miss 不能只看 L3 miss要看到 DRAM 的 trafic 以及 SLC miss。很多时候 L3 miss 之后命中了 SLC性能并不会拖垮。2.2 多 Bank、多 Slice 的设计为什么 SLC 能扛住高并发SLC 在物理上并不是一块大 SRAM而是被切成多个 slice 和 bank。CMN-700 的 SLC 会根据物理地址哈希把连续地址空间分布到不同的 slice 上这样多个 CPU 核心同时访问不同的数据时可以并行命中不同 slice避免单缓存体成为瓶颈。哈希策略是 SLC 设计里最值得研究的点。理想情况下任意一段连续数据都会被均匀打散到所有 slice 上让每个 slice 的访问压力接近。实际工程里哈希不仅看地址的低位还可能要结合地址高位、甚至 requestor ID目的就是避免某些 slice 过于热闹、某些 slice 闲得没事干。我记得自己第一次调 SLC 相关参数时抓 SLC 热分布发现某几个 slice 的 hit 数远高于其他 slice后来验证是因为一个加速器频繁访问固定物理页哈希策略没把它打散。这种情况如果不用 SLC 分区做隔离它会影响在同一 slice 上的其他核心请求。多 bank 的设计也很重要。同一个 slice 内部可能有多个 bank每个 bank 支持独立的读写端口。这样即使多个请求命中同一个 slice也能根据 bank 并行处理。SLC 的延迟并不像 CPU 私有 cache 那样极致但吞吐量非常可观。片上网络里的请求和数据响应是分离的所以 SLC miss 时会先从 request network 到内存控制器数据返回再到 SLC整个过程可以流水线化。2.3 一致性协议与 Snoop 过滤SLC 不是简单的 “Cache”还是 “协议大脑”SLC 不仅仅是存放数据它还要维护所有接入设备的一致性视图。在 CMN-700 中每个 MNMesh Node都有对应的寻路逻辑HN-FHome Node负责管理某段物理地址的一致性RN-FFully Coherent Request Node负责发出缓存一致性请求。SLC 通常和 HN 绑定HN 内部维护 snoop filter 和 tag 状态记录某个 cacheline 被哪些 RN 以什么状态持有。这就好比图书馆的借阅系统。SLC 里的 tag 相当于图书索引它知道哪本书被谁借走了、是可共享还是独占。当一个核心要写数据时SLC 会向所有可能的持有者广播 snoop 请求确认没有其他副本然后才允许写。如果 SLC 没有这项过滤那每次访问都要问遍每个 cluster片上网络的流量会爆炸。CMN-700 做的 snoop filter 就是通过目录快速定位减少不必要的广播。这里值得留意的是SLC 在一致性协议中并不是一个单纯“被动”缓存它也会接收来自 IO 设备的一致性请求。比如网卡使用 DMA 写入一块缓冲区它会通过 PCIe 控制器发出请求到 CMN-700SLC 必须保证之前 CPU 对该缓冲区的写操作已经可见否则网卡可能读到脏数据。这个场景在高速网络处理中尤其常见也是排查“数据不一致”问题时最先怀疑的地方。3. 缓存分区技术解决的是一次真实的“邻居噪音”问题3.1 为什么服务器场景必须做缓存分区服务器上跑的负载可不像手机 App 那么单一。一个物理机上可能同时运行多个虚拟机数据库、Web 服务、AI 推理可能共享同一个 SLC。默认情况下所有核心对 SLC 的访问是公平竞争的。一旦某个高带宽负载疯狂占用 SLC其他负载的命中率就会下降延迟随之升高。这就是典型的邻居噪音noisy neighbor问题。假如你在裸金属上跑了一个延迟敏感型的交易服务旁边另一个核却在进行全表扫描它会不断请求大量新数据把 SLC 里的热数据全部挤出去。没有缓存分区时交易服务只能一次次 miss 到 DRAM尾延迟直接报销。CMN-700 提供的缓存分区能力就是允许你划分出“车道”让关键负载的缓存空间不被其他负载侵占。用公路来类比SLC 原来是一条所有人都可以走的单车道总有人堵路。缓存分区则是把它改成了多车道每一个数据中心租户或关键进程拥有专属车道旁边车道再堵也不会影响你。不过车道一旦分隔也有可能浪费空间所以分区策略需要在隔离和利用率之间平衡。3.2 基于 Way 的分区方式到底分了什么CMN-700 的 SLC 一般是组相联结构有多个 Set每个 Set 里有多路 Way。缓存分区最常见的方式就是按 Way 划分容量。你可以把 SLC 的所有 Way 分成若干组将某几个 Way 分配给某个 requestor其他 Way 分配给另外的 requestor 或保持共享。这种方式的好处是物理隔离很干净不会出现某个 requestor 把别的 partition 的数据踢出去的问题。具体到 CMN-700 的寄存器配置层面每个 requestor 都可以有一个 Way 掩码表示允许使用哪些 Way。例如 SLC 总共 16 路你可以设置 requestor A 只能使用 way0-way7requestor B 只能使用 way8-way15。这样 A 的所有分配和替换都只会发生在自己的 8 路空间内。但要注意Way 分区只限制了“分配”行为。也就是说一个核心访问了不属于它的数据如果数据已经在 SLC 中它仍然可以命中只有当发生 miss 并需要往 SLC 分配新行时才必须落在自己允许的 Way 掩码范围内。所以它不会阻止你看到其他 partition 的数据只是不允许抢占对方的缓存空间。3.3 从 QoS 角度看缓存分区和请求仲裁缓存分区是空间隔离QoS 则是时间维度的优先级控制。CMN-700 的 QoS 机制可以在片上网络和 SLC 的请求队列中设置优先级让高优先级请求插队从而降低延迟。实际部署中最好结合缓存分区和 QoS 一起做。空间上给关键负载留出专属 Way时间上给它更高的仲裁优先级。比如一个数据包处理应用不希望被后台备份任务阻塞可以给它的 requestor 打上高优先级标记同时保留固定 Way 区域让它的命中率稳定。这里有个原则优先级解决的是“同时竞争时谁先走”Way 分区解决的是“长期缓存空间占用比例”。两者不能互相替代。4. 分区配置与调优的实际操作思路4.1 开始之前先梳理你的业务模型配置缓存分区前要先回答几个问题系统里有几个真正需要隔离的实体每个实体的 SLC 容量需求是多少它们之间的流量是持续高带宽还是偶发突发是 CPU 核心还是 IO 设备主导以我调试过的一个 NFV 场景为例同样一个 SoC 上要同时跑控制面和高性能数据面。控制面需要低延迟但流量不大数据面是高吞吐但可以由队列管理。这种情况下我给数据面预留较大比例的 SLC Way给控制面留较少但稳定的 Way。如果不能确定比例可以先用默认的公平共享跑一个基准观察每个 requestor 的 miss 情况再根据 miss rate 做调整。另一个比较常见的场景是数据库和 AI 推理混部。数据库希望尽量多的 SLC 给热点索引AI 推理则主要是卷积和矩阵运算数据流比较规整但也需要一定缓存空间。这里不能简单地把 SLC 全部给数据库因为 AI 推理的重复访问同样受益于 SLC。根据实测数据我通常会先分出 1/4 的 Way 给 AI 推理如果它的算力空闲期较长这些 Way 还可以通过动态方式重新释放回共享池。4.2 关键配置路径从寄存器到固件CMN-700 的缓存分区配置本质上是对内存映射寄存器做读写。不同 SoC 的基地址不一样但寄存器布局以 Arm 的 CMN-700 技术参考手册为准。配置过程一般分成三步找到 CMN-700 的配置空间基地址以及对应 root node 的寄存器。确定目标 requestor 的节点类型和 ID。例如某个 DSU cluster 的 RN-F ID或者一个 PCIe RC 的 RN-I ID。写入 Way 掩码使能分区。一个典型的写法可能是# 假设配置空间基地址为 0x400000000000RN-F ID 为 0x10 # 将 SLC 的 way8-way15 分配给 RN-F 0x10 /dev/mem 方式下写寄存器需要极大谨慎实际工程中通常由固件或驱动完成 # 伪代码示意 cmn_write(0x400000000000 RN_F_NODE_OFFSET SLC_WAY_MASK_REG, 0x0000FF00)这里我不会给出具体的物理地址因为不同 SoC 对 CMN-700 的实例化差异很大。你真正要做的是去找你手上芯片的 TRM里面会有“CMN-700 Programming”章节。寄存器名字也常常因 SoC vendor 稍有不同有些还会封装到 ATFARM Trusted Firmware或 UEFI 里。如果你用的是标准 Arm 开源软件可以关注cmn-700相关的驱动和 DTS 配置。还有一个很容易踩的坑SLC Way 掩码是每个 requestor 单独设置的但你没法保证所有 Endpoint 都主动设置未设置的话使用默认掩码默认一般是全 1也就是可以使用所有 Way。只有你显式给所有需要隔离的请求方都配上掩码分区才能真正生效。漏配一个它就可能进入共享空间挤掉其他分区。4.3 如何验证分区真的生效了配置完分区不能只是看看寄存器读回值那样只能确认写入成功不能确认性能行为真的隔离。我常用以下办法验证观察 SLC hit/miss 计数CMN-700 的 Performance Monitor 可以按 requestor 统计 cache hit、miss、access。如果分区生效在压测高带宽 load 时关键负载的 miss rate 不应明显劣化。监控 DRAM 带宽占用如果某个分区被隔离后它仍然频繁 missDRAM 带宽会持续走高。这可以反推是不是给的 Way 容量不够。用延迟直方图判断低延迟负载的 p99 应该保持稳定。如果 p99 明显漂移说明可能还有别的流量在干扰。性能计数器在 Linux 下不一定都能直接看到但很多 SoC 会在 perf 事件列表里暴露部分事件。比如arm_dsu事件、CMN 事件需要厂商补丁支持。没有 perf 支持的话可以走 SoC vendor 提供的 profiling 工具直接读取 CMN-700 的 PMU 寄存器。4.4 动态分区和静态分区怎么选静态分区配置简单Run 起来之后不改变适合负载相对固定、对延迟要求极其严格的场景。动态分区则需要软件参与根据实时负载调整 Way 掩码。动态分区的优点是可以最大限度利用 SLC 容量缺点是调整过程中可能引发抖动。如果你的关键负载能容忍偶尔的延迟毛刺可以考虑动态调整如果这是交易系统或实时控制面我建议静态分区更稳妥。还有一种混合方案核心分区固定不变额外的空闲 SLC Way 作为一个“弹性池”非关键负载可以用但在关键负载需要时通过 QoS 抢占。实际项目中我偏好在 bios/固件阶段就把静态分区配好同时预留一个未分配的共享 Way 池给那些不需要强隔离的小流量使用。这样既能稳定关键负载又不会造成太多缓存浪费。5. 常见问题与排查技巧实录5.1 “我明明配了 Way 掩码为什么还有跨 partition 的访问”先检查一下 Requestor ID 是不是理解错了。RN-F 不仅仅是整个 DSU cluster有时候不同的 CPU cluster 可能有多个 ID。默认掩码是全部 Way如果你只给其中一个 ID 配置了掩码其他核心仍然可以全 Way 访问。按我踩坑经验配置要覆盖所有可能产生高流量的 requestor包括 PCIe 控制器和加速器。另一个原因是某些访问路径是绕过 SLC 的。比如一些 non-cacheable 的 DMA 访问直接走内存控制器根本不查 SLC所以也不会受 Way 掩码限制。这时候你看到的流量是真实存在的但不是 SLC 分区能管住的。5.2 SLC 分区之后命中率反而下降了缓存分区本质上把 SLC 拆成了多个小缓存。小缓存的总容量没变但有效容量变少了。如果某个分区容量太小放不下工作集命中率下降是必然的。你没办法通过调整 Way 掩码来凭空提高总容量只能重新划分或者把工作集缩小。举个简单的例子你的关键负载工作集需要 12MB SLC但你只给它分了 8MB其他负载用了 16MB。虽然隔离效果很好但关键负载的 miss 率会比原来共享时还高因为原来它允许使用全部 24MB。这是分区设计的核心权衡为了隔离你牺牲了部分缓存效率。理想情况下分区的总 Way 数应该大于关键负载的工作集需求否则还不如不分区。5.3 内存带宽被一个 PCIe 设备占满SLC 分区能不能解决可以缓解但不能完全替代带宽 QoS。SLC 分区控制的是缓存空间如果一个 PCIe 设备持续做高带宽 DMA即使它的数据不缓存也会把内存控制器带宽吃满。CMN-700 的 QoS 机制里有带宽Bandwidth Regulation相关的功能也可以限制某个 endpoint 的请求速率让关键路径不被抢占。在调优实践中我会同时做三件事给网卡/加速器分配合理的 SLC Way设置较低的 QoS 优先级再在内存控制器层面对总带宽做配额。三层配合才能把延迟毛刺压下去。5.4 多 die 或多 CMN-700 的平台上分区配置会互相影响在更复杂的 SoC 封装里可能存在多个 CMN-700 实例或者是 multi-die 设计每个 die 有自己的 SLC。跨 die 访问时请求需要通过 die-to-die 互联到另一个 CMN-700这时的缓存分区边界会变得模糊。如果你的负载经常访问 remote die 的内存本地 SLC 分区可能只对本地访问有效而对远端访问没有明显帮助。遇到这种情况更要依赖系统级 profiling 工具确认数据到底落在哪个 die、哪个内存控制器。不要只盯着本地 CMN-700 的计数器看。最好让同一个虚拟机和它的数据尽可能绑核、绑内存节点减少 remote SLC 的介入。5.5 我知道分区参数但系统起来后寄存器值被固件覆盖了这是很多人会碰到的问题配置写在某个用户态测试工具里但固件在启动过程中重新初始化了 CMN-700导致你的设置失效。所以我建议分区的初始配置放在启动阶段比如由 ATF、UEFI 完成或者使用 SoC vendor 提供的标准配置接口。如果非要运行时调整要确保固件不会二次写覆盖同时要做好随时读取确认的准备。6. 关于 CMN-700 SLC 调优的一些个人体会做缓存分区调优不像调 CPU 频率那样立刻见效。SLC 的表现需要结合整体业务流量来看而且很可能需要在多个指标之间做取舍。比如你牺牲了一部分非关键负载的缓存空间换取了关键业务尾延迟稳定这个收益可能无法用平均性能来衡量但对在线服务质量至关重要。我个人的习惯是先把数据摸清楚再动分区。至少要掌握每个 requestor 的 SLC miss、hit 和 DRAM 带宽占用才能知道是不是缓存空间不足导致的延迟升高。否则一上来就分 Way可能只是把一个透明的问题变成了一个更复杂的配置问题。另外缓存分区的配置要留可回滚的余地。每一次调整都记录下 Way 掩码和 QoS 参数并做好性能基线对比。这样即使线上出了问题也能快速还原到上一个稳定状态。最后分享一个我踩过的小技巧给 SLC 分区时不要按比例去分要按“工作集大小 一定余量”去分。可以长时间跑真实负载然后用性能计数器看一下关键负载的 SLC hit footprint得到它实际需要的容量再在此基础上多留 10%~20% 的余量。这样分区既隔离得稳又不会浪费太多缓存空间。如果后续负载特征变化需要重新计算工作集再动态调整分区即可。调优这种事没有一步到位的灵丹妙药但方向对了效果一定能看到。