
1. 先把核和核外面的东西分开一张架构解剖图很多人第一次跑性能分析时都会遇到同一个困惑任务管理器或者top显示 CPU 利用率只有 40%但程序就是跑不快或者明明把线程数量调到了物理核数吞吐量却卡在一个奇怪的数字上不动了。我见过最多的误判是把CPU 占用率直接等同于CPU 用满了。实际上现代处理器的核心Core和核心之外的部分Uncore是两套完全独立的资源占用率只统计了前者而后者一旦成为瓶颈你在监控面板上几乎看不到任何异常。Core 负责执行指令Uncore 负责把数据搬运到 Core 手边两者的频率、电压域、功耗预算和性能归属都是分开的。搞不清这条分界线后面所有关于 P-Core、E-Core 的讨论都是空中楼阁。这篇文章想聊的就是这条线。我会先拆开看 Core 和 Uncore 各自装了什么再解释 P-Core性能核和 E-Core能效核到底差在哪些微架构细节上然后落到实操怎么用几条命令把自己机器的大小核拓扑照出来怎么绑核怎么读 uncore 的性能计数事件以及我在实际项目里踩过的几个抖动坑。内容偏底层的性能工程方向但只要你会用命令行、跑过top就能跟着走下来。没有悬空的理论每一条结论后面我都会说清楚它是从哪个计数器或者哪次压测里来的。1.1 Core 里到底装了什么一颗物理核的内部可以粗糙地理解成前端 后端 私有缓存三段流水线。前端负责把指令从内存里取出来经过分支预测、译码变成微操作塞进乱序窗口后端有整数单元、浮点单元、访存单元按照数据依赖关系乱序执行私有缓存是 L1 指令缓存、L1 数据缓存和每个核独享的 L2 缓存。这三段全部是每个核一份互不共享所以核的数量直接决定了并行执行的能力上限。你看到的物理核数说的就是这个独立流水线的份数。关键点在于Core 的性能上限是被它的发射宽度和乱序窗口深度锁死的。一颗宽度为 6 发射、乱序窗口能挂 500 多条微操作的核和一颗宽度为 3 发射、窗口只有一半的核在跑同一个单线程程序时IPC每周期执行的指令数能差出一倍以上。这个差距跟频率无关是纯粹的结构差异。所以你看到两个核频率差不多但跑分差很多的时候别急着怀疑散热先去看它们的微架构是不是同一个档次。还有一层容易被忽略Core 内部有自己的一套电源管理和时钟门控。没有任务的核会被深度降频甚至部分断电这就是为什么你在一台 32 核的机器上跑一个单线程程序看到只有一个核在 3.5GHz 以上高频运行其他核显示 800MHz。这不代表你的机器有问题反而说明调频策略在正常工作。真正需要警惕的是相反的情况——所有核都显示高频但程序还是慢那基本可以确定瓶颈已经跑到 Core 外面去了。1.2 Uncore 不是打杂的它是数据的高速公路Uncore 是个常被误解的词听起来像是次要部分实际上它包含了所有核共享的关键基础设施。按重要性排主要是这么几块末级缓存LLC也就是通常说的 L3、内存控制器、核与核之间的互连总线环形或者网格状拓扑、对外的高速接口控制器、集成显示单元、系统代理以及统一管理整颗芯片频率和功耗的电源管理单元。这些部件的共同特征是共享——不属于任何一个核但所有核都要经过它。为什么这个划分值得单独拿出来讲因为它的频率是独立配置的。Core 频率和 Uncore 频率各有自己的倍频在 BIOS 里通常是两个分开的选项。很多主板默认把 Uncore 频率设成自动在轻载时为了省电会把它压得很低这时候 Core 还在高频跑但每次访问 LLC 未命中、每次读内存的延迟都被拉长了。表现出来就是主频看着很高跑分却没提升游戏帧数还会莫名其妙地周期性掉一下。我做过一组对照测试同一台机器同一颗芯片只改 Uncore 频率限制。在内存密集型负载下多线程随机读Uncore 从默认的自动档拉到固定高频吞吐量提升了接近 18%而 Core 频率完全没动。这个数字放在单线程计算密集型负载上则几乎为零。这就直接告诉你一件事判断要不要动 Uncore先看你的负载是不是吃内存带宽或者缓存延迟。如果你的程序全部命中的是 L1/L2那 Uncore 怎么调都无所谓一旦开始大量走 LLC 和内存它就变成第一优先级。顺带说一个副产品内存带宽的理论上限其实是个算术题跟 Uncore 关系不大但很多人算错。以双通道、4800MT/s、每通道 64 位为例上限是 2 × 4800 × 8 字节 76.8GB/s。但这是物理层的理论值实际能跑到 65%75% 就不错了剩下的损耗来自协议开销、刷新周期和 Uncore 侧的调度效率。所以你在压测里看到 50GB/s 就以为被限制了很可能只是正常损耗而不是哪里没配对。1.3 为什么这条分界线决定了你后面所有的调优动作把 Core 和 Uncore 分开之后很多玄学现象就有了统一的解释框架。举个例子一个延迟敏感的在线服务P99 延迟时不时飙到几十毫秒CPU 利用率很平稳。如果你只盯着 Core会得出CPU 没问题的结论然后开始怀疑网络、怀疑锁争用。但如果看一眼 Uncore 侧的内存控制器计数器和 LLC 未命中率很可能发现是另一条业务线的批量任务在周期性地冲刷 LLC把你的热数据全挤出去了。这时候的修复方案不是加核而是做缓存隔离或者错峰调度。再比如虚拟化场景。很多虚拟化平台把每个虚拟 CPU 映射到一个物理核但 Uncore 是宿主机层面共享的客户机里面完全看不到。结果就是客户机的性能随邻居的负载变化而波动而你在客户机内部查任何指标都是正常的。这类问题不搞清楚 Core 和 Uncore 的分层排查方向会一直跑偏。所以我后面讲 P-Core 和 E-Core 的时候会一直用这个框架Core 层面比的是单核执行效率Uncore 层面比的是共享资源供给能力。你遇到任何性能问题第一件事就是判断它归到哪一层答案通常在五分钟内就能定下来。2. P-Core 与 E-Core不是快慢之分而是面积-延迟-吞吐的三角取舍大小核这个词被用了很多年但真正理解的人不多。最常见的误解是P 核就是大核E 核就是小核小核是缩水版。这个描述不算错但它掩盖了设计动机。E-Core 从来不是做不好的 P-Core它是为了另一个目标函数优化出来的产物在同样的硅片面积和功耗预算下最大化多线程吞吐。这两个目标在微架构层面是冲突的所以必须做成两种核。理解了这个冲突你才能明白为什么调度器这么难写以及为什么有些任务绑到 E 核上反而更稳。2.1 两种核心的微架构差异摊开看把两种核放在一张表里对比差异会非常直观。下面这些数字不是某一款具体产品的参数而是这类混合架构的典型量级你手上的机器可能略有出入但比例关系是稳定的维度P-Core性能核E-Core能效核发射宽度宽6 左右窄3 左右乱序窗口深500 微操作浅200 左右同时多线程支持一个核跑两个逻辑线程通常不支持私有 L2每核独享容量大多核成簇共享单核可见容量小峰值频率高明显低一档单核面积基准 1 份约为 P 核的 1/4分支预测器大容量、多级精简版宽向量指令支持部分型号不支持或降速执行表格里最值得盯的是乱序窗口和同时多线程这两行。乱序窗口决定了处理器能在多大范围内重排指令来掩盖延迟窗口越深遇到缓存未命中和长延迟操作时越从容。这就是为什么同样跑一个分支密集、随机访存的程序P 核的 IPC 能做到 E 核的两倍以上。同时多线程的意义在于当一个线程因为等待数据而停顿时同一个核上的第二个线程可以填补执行空隙把流水线利用率从 40% 拉到 70%。E 核没有这个机制它靠的是数量——单个效率低但占地小可以堆很多个。还有一个经常被忽略的差异E 核通常是成簇共享 L2的。四个 E 核共享一块 L2意味着它们之间的数据交换比跨簇快得多但同时也意味着缓存容量是按簇计算的不是按核。如果你的程序在同一簇的 E 核之间频繁共享数据性能会比分散绑到不同簇上明显更好。这一点在做线程亲和性设计时非常有用后面第三章会具体讲怎么看出来。2.2 面积经济学为什么厂商愿意用一个大核的地盘换四个小核一个 P 核的硅片面积大致能做四到五个 E 核这个比例是整个混合架构的经济基础。假设你有固定的面积预算方案 A 是全做 P 核你能拿到 8 个核 16 个线程方案 B 是 4 个 P 核加 16 个 E 核总共 20 个核 24 个线程。单线程性能方案 A 略强因为 P 核可以跑到更高频率热密度也更集中但多线程吞吐方案 B 能拉开 30% 以上的差距因为线程数接近翻倍。这就解释了为什么厂商愿意承担调度复杂度也要做混合架构在消费级场景里绝大多数日常负载的线程数远超 8 个而真正时长超过几秒的计算密集单线程任务反而很少。用面积换吞吐在统计意义上几乎总是划算的。代价是软件侧要处理异构性这笔账被转嫁给了操作系统和开发者。但这里有个反直觉的结论我在实际测试里验证过很多次E 核的能效优势不是绝对的。它的单位功耗性能确实更高但因为它更慢同一个任务跑完需要更长时间总能耗未必更低。只有当任务是多线程并行、能把所有核都喂饱的时候E 核的能效优势才明显体现出来。如果你把一个固定工作量的任务放到 E 核上跑它可能用更低的功率跑了两倍的时间总电量反而更多。所以能效核这个名字应该理解成单位面积单位功耗的吞吐效率高不是更省电。这个认知直接影响调优策略。我看到不少人为了省电主动把后台服务全部绑到 E 核上结果这些服务因为延迟变高、超时重试增多整体功耗反而上升了。正确的做法是延迟敏感的服务留在 P 核批处理、日志压缩、索引重建这类吞吐型任务丢给 E 核。这样两类任务的资源需求刚好互补也让调度器的决策变得简单。2.3 混合架构真正的难题调度器怎么知道该把线程放哪硬件层面有一层机制在解决这个问题处理器会实时采集每个核的负载特征通过一个小型的状态表告诉操作系统这个线程适合放在哪类核上。它评估的东西包括线程占用多少资源、是否在用宽向量指令、是否频繁等待内存、以及核心当前的热状态。操作系统拿到这些提示后再叠加自己的调度策略做最终决定。这层协作机制是整个混合架构能否跑好的关键也是各家实现差异最大的地方。理解它的工作方式对你排查问题很有帮助。举个真实的例子我在一台混合架构机器上编译一个大项目用make -j全核并行。理论上应该比纯 P 核机器快但实测慢了 15%。查下来原因在于编译器在某些源文件上会使用宽向量指令而这些线程一旦被迁到不支持该指令集的小核上就要走软件的兼容路径速度掉一大截。操作系统虽然有补偿机制会尽量避免把这类线程迁走但在负载剧烈波动时还是会发生。解决方案很简单把编译任务限制在 P 核上跑。做完之后不仅速度快了波动也消失了。另一个常见症状是频率不一致导致的时间测量失真。你在两个线程里各跑一遍同样的计算一个落在 P 核上花了 100 毫秒另一个落在 E 核上花了 180 毫秒如果你的程序用谁先完成来做逻辑判断就会产生完全不可复现的行为。这类 bug 极难定位因为它在纯同构机器上永远不会出现。我的建议是凡是依赖执行速度做判断的逻辑比如超时阈值、竞争选举、负载均衡策略都必须显式地在代码里做归一化或者干脆用逻辑时钟替代物理时钟。最麻烦的场景是虚拟化。虚拟机里通常看不到核心类型的区分宿主机的调度器把 vCPU 映射到哪种核上客户机完全不知情。这就造成了一个典型的性能谜题同样的镜像、同样的配置两台宿主机上的性能差 20%。要解决只能在宿主机层面做绑定把关键虚拟机的 vCPU 钉在 P 核上或者至少保证同一个虚拟机的 vCPU 落在同类核上。3. 把拓扑照出来四类命令快速摸清自己的机器理论讲完了落到手上。每次接手一台新机器我做的第一件事永远是摸拓扑因为这个动作成本极低但能避免后面一大堆误判。下面这几条命令覆盖了从有哪些核到Uncore 在干什么的全部信息顺序建议照着我写的来因为信息是逐层深入的。3.1 一分钟看清核心布局第一条命令是lscpu输出里会直接列出架构、物理核数、逻辑核数、NUMA 节点数和缓存层级。混合架构的机器上你会看到每核线程数这一项不统一——如果它显示 1同时核数等于线程数那很可能全是 E 核或者关了超线程如果物理核数和逻辑核数的比值不是整数倍关系那基本可以确认是混合架构。接着用扩展模式拿到更细的信息lscpu -eCORE,SOCKET,NODE,MAXMHZ,MINMHZ,L1D:L1I:L2:L3,ONLINE这条命令的输出是逐逻辑 CPU 一行的表格。判断哪些是 P 核最直接的方式是看MAXMHZ列——P 核的最高频率通常比 E 核高出 40% 以上一眼就能分出来。另一个线索是CORE列如果两个逻辑 CPU 共享同一个 CORE 编号说明它们是一对超线程兄弟而这种情况只会出现在 P 核上。如果你想要更图形化的呈现用lstopo-no-graphics属于 hwloc 工具集。它会把 NUMA 节点、缓存层级、每个核的兄弟关系画成树状结构还能显示每个核到内存控制器的距离关系。我在做跨节点性能分析时基本离不开它因为只有这张图能直观地告诉你这两个核共享什么、不共享什么。再补一条常被忽略的查看超线程兄弟对的具体编号。for i in $(seq 0 $(($(nproc)-1))); do echo cpu$i - $(cat /sys/devices/system/cpu/cpu$i/topology/thread_siblings_list) done输出里每一对相同的数字就是一对兄弟。做延迟敏感型绑核时这个信息决定成败因为把两个高负载线程放到一对兄弟上它们会争抢同一个核的执行单元实测性能损失能到 30%。更隐蔽的是有些机器的编号排列不是0-1 是兄弟而是0-8 是兄弟你凭直觉写taskset -c 0-1恰好就把两个线程塞进了同一个核。3.2 从 sysfs 里读出 Uncore 与频率域/sys/devices/system/cpu/这个目录是台金矿。几个必看的位置# 每颗核支持哪些频率档位 cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_available_frequencies # 当前的调频策略powersave / performance / schedutil 等 cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor # 缓存共享关系判断哪些核共享 L2 cat /sys/devices/system/cpu/cpu0/cache/index2/shared_cpu_list最后一条特别有用。混合架构上P 核的 L2 是每核独享的shared_cpu_list只有自己而 E 核成簇共享 L2shared_cpu_list会列出同簇的所有逻辑 CPU。这样你不用猜直接就能读出 E 核的簇划分。我在做线程池亲和性设计时会直接把同一簇的 E 核作为一组让组内的线程共享数据结构跨组只做粗粒度通信这个改动在一个数据聚合服务上带来了约 12% 的吞吐提升。Uncore 频率的读取在较新的内核上有专门的 sysfs 节点路径包含uncore_frequency字样里面通常有min_freq_khz、max_freq_khz、current_freq_khz这几个文件。可以直接写max_freq_khz来锁定上限这是免进 BIOS 就能调 Uncore 的最省事的方式。实时监控我推荐turbostat它输出的表格里有Bzy_MHz核心实际频率、UncMHzUncore 实际频率、PkgWatt整包功耗这几列。盯着Bzy_MHz和UncMHz的比值看就能判断当前负载是卡在核心上还是卡在 Uncore 上。经验值是这样的如果Bzy_MHz已经跑到接近规格上限而UncMHz稳定在额定值说明是算力瓶颈如果Bzy_MHz忽高忽低、UncMHz也有明显波动那大概率是共享资源在抢。3.3 PMU 事件把 Core 和 Uncore 的账分开算性能计数器是分清两层界限的终极手段。现代处理器的 PMU 分为两部分核心内的计数器用来数周期、指令、分支预测失败、L1/L2 访问Uncore 的计数器用来数内存控制器吞吐、LLC 命中与缺失、互连总线流量。在perf里这两套事件的命名前缀不一样以uncore_开头的都属于 Uncore 侧。先看有哪些可用的perf list | grep -i uncore你会看到诸如内存控制器的读带宽、写带宽、LLC 查找次数、跨片互连流量这类事件。数量不多但每一个都直指共享资源的供给能力。接下来的典型用法# 核心侧IPC 和缓存效率 perf stat -e cycles,instructions,branches,branch-misses,L1-dcache-load-misses,LLC-load-misses -p pid # Uncore 侧内存带宽事件名视平台而定照 perf list 的输出替换 perf stat -e uncore_imc_0/cas_count_read/,uncore_imc_0/cas_count_write/ -a sleep 5第一次跑的时候要注意两个坑。第一Uncore 事件通常是整包级别的需要-a全系统模式不能只看单个进程因为它统计的是共享硬件的行为没法归因到某个进程上。第二在虚拟机里这些计数器绝大多数是禁用的你看到event not supported是正常的别浪费时间排查。有了这两组数据判断路径就很清楚了。用下面这张表做个快速索引观测到的现象判断方向下一步动作IPC 低L1 未命中率高算法访存模式差优化数据结构提高局部性IPC 正常LLC 未命中率高工作集超过 LLC分块处理或做缓存隔离LLC 未命中率正常内存带宽接近上限Uncore 供给不足提 Uncore 频率、加通道、改 NUMA 策略指令数突增但周期数没变走了兼容路径检查是否被调度到不支持某指令集的核上核心利用率低Uncore 流量高数据搬运开销大减少跨节点访问检查互连流量我排查过一个程序变慢但 CPU 不满的案例最后就是靠第三行定位的LLC 未命中率在正常范围但内存控制器的读带宽已经打到了理论上限的 92%。也就是说 Core 本身效率没问题是 Uncore 喂不饱这么多个 Core。这种时候唯一有效的方案是把工作集缩小到能放进 LLC 的尺寸或者干脆减少并发核数——听起来反直觉但并发变少后每个线程拿到的带宽更多总完成时间反而缩短了。4. 绑核不是万灵药把线程放到对的位置上绑核这个动作被用错的频率远高于被用对。我看到过太多人一遇到性能抖动就加taskset结果把本来是负载不均衡的问题变成了资源闲置。绑核的本质是控制变量你放弃了调度器动态调整的自由度换取确定性。所以只有在确定调剂动态性是干扰源的时候绑核才是正解。判断标准很简单如果你的问题是数值不可复现绑核有用如果问题是整体不够快绑核通常没用甚至有害。4.1 延迟敏感型任务的标准姿势在线服务、交易系统、实时数据处理这类任务的核心指标是 P99 而不是平均值。它们最怕的东西是意外——意外的迁移、意外的抢占、意外的缓存失效。针对这类负载我用的标准配置是这样的# 1. 选出 P 核避开超线程兄弟对 # 假设 P 核是 cpu 0-15兄弟对为 (0,8) (1,9) ... # 只取每对的第一个得到 0-7 # 2. 绑核 绑内存节点 taskset -c 0-7 numactl --membind0 ./service # 3. 把中断挪到 E 核上避免打断关键线程 echo 16-23 /proc/irq/default_smp_affinity_list这三步的顺序很重要我分开解释为什么。第一步只取每对超线程的第一个是因为同一对兄弟共享执行单元和 L1/L2让两个高负载线程挤在一对兄弟上效率损失比多用一个物理核大得多。第二步把内存绑定到本地节点是为了避免跨节点访问——跨节点的一次内存访问延迟能从 80 纳秒涨到 140 纳秒以上对延迟敏感型任务这是致命的。第三步把中断赶走是因为一个中断处理看起来只有几微秒但它会污染目标核的 L1/L2让后面几百微秒内的缓存全部失效。第三步的收益经常被严重低估。我在一个消息处理服务上做过对照其他条件不变只把网络中断从业务线程所在的核上挪走P99 延迟从 4.2 毫秒降到 1.8 毫秒降幅超过一半。原因就是中断处理会冲刷缓存而缓存预热需要时间。这个改动的成本几乎为零收益却非常可观我建议所有延迟敏感型服务都检查一遍。还有一个进阶手段用 cgroup 的 cpuset 做隔离比taskset更彻底因为它能防止其他进程被调度到这些核上。新版 cgroup 里可以给 cpuset 设置独占分区效果类似于内核启动参数里的核隔离但不需要重启。容器化部署时这个方案特别合适因为隔离策略可以跟着容器一起声明。4.2 吞吐型任务别急着关 E-Core另一个极端是把 E 核全部关掉。这个做法的动机可以理解——消除异构性让性能可预测。但我实测下来在吞吐型负载上关掉 E 核基本是净亏损。做过一组对比一个日志分析和聚合的批处理任务在全核含 E 核上跑用 42 秒关掉 E 核只用 P 核跑用 51 秒。也就是说那些慢核贡献了 18% 的吞吐白扔掉很可惜。正确的做法不是关核而是让任务匹配核的特性。我给这类任务设计线程池时的策略是这样的主控线程和 I/O 线程放在 P 核保证响应速度工作线程按批次分发给 E 核因为它们做的是纯计算、无依赖、可并行的活。这样既拿到了 E 核的数量优势又不会因为调度错位而拖慢关键路径。具体实现上可以用两个独立的任务队列配合不同的亲和性设置# 主控 IO 线程池 taskset -c 0-3 ./app --io-threads4 # 计算线程池绑到 E 核簇上假设 E 核是 16-23 taskset -c 16-23 ./worker --concurrency8这里有个细节E 核的簇划分要和线程数对齐。如果 E 核是 4 核一簇、共两簇那绑 8 个线程是合适的如果你绑 6 个就可能出现某一簇被塞了 4 个、另一簇只有 2 个的不均衡情况实际性能比绑 4 个还差。这个坑我踩过一次线程数从 8 改成 6 之后吞吐掉了 20%查了很久才发现是簇内竞争导致的。还有一类任务特别适合 E 核长时间运行但不追求单次速度的后台任务比如日志轮转、索引合并、缓存预热、健康检查。这些任务的特点是优先级低、可以容忍延迟、但数量多。把它们绑到 E 核上既释放了 P 核又避免了它们被调度器到处搬来搬去打扰关键业务。4.3 中断、容器与虚拟化里的三个坑第一个坑是容器的 CPU 限制和亲和性冲突。给容器设了--cpus4之后运行时会在宿主机层面做带宽限制但如果你同时又在容器内用taskset绑了 4 个核而这 4 个核恰好跨了 NUMA 节点那你既失去了调度的灵活性又引入了跨节点访问。我建议容器场景只保留一套约束要么用 cpuset 做硬绑定同时解决配额和亲和性要么只做配额限制让调度器自由发挥。两套机制叠加是最容易出问题的组合。第二个坑是虚拟机的 vCPU 与物理核错配。有些平台默认把 vCPU 均匀打散到所有物理核上这在混合架构下会导致同一个虚拟机的 vCPU 有的在 P 核有的在 E 核客户机内部看到的同构多核其实是假的。表现是客户机里的并行程序性能波动极大而且无法通过客户机内部的任何手段解决。方案是在宿主机层面配置亲和性规则保证同一个虚拟机的 vCPU 落在同类核上。这个改动在编排层面做一次就行属于一劳永逸的收益。第三个坑是中断亲和性与绑核的隐性冲突。你在业务线程上做了绑核但中断控制器仍然可能把中断送到这些核上尤其是当你只绑了几个核而系统有几十个核的时候中断分布会很不均匀。检查方式是看/proc/interrupts里每个核的中断计数如果发现某个核的计数远高于其他核那就是没配对。修复方式是把中断亲和性显式设置为不包含业务核的那组 CPU。这个动作在压测前做一次能消除相当一部分的延迟毛刺。5. 三个真实抖动案例的排查链路光讲原理容易飘我挑三个自己实际处理过的案例把完整的排查路径摊开。这三个案例的共同点是表面症状相似都是变慢但根因分别落在 Core 层、Uncore 层和调度层处理方式完全不同。我特意保留中间走过的弯路因为排查过程本身就是最有价值的部分。5.1 案例一单线程程序越跑越慢现象一个计算密集的单线程任务跑第一次用 30 秒第二次 33 秒第三 次 37 秒重启进程后又回到 30 秒。这个模式非常反常因为纯计算任务不应该有累积效应。第一反应是内存泄漏或者 GC 压力但监控显示内存平稳。第二步怀疑降频用turbostat盯着看果然发现了问题任务运行 30 秒后核心频率从 4.2GHz 掉到 3.4GHzPkgWatt也接近了功耗上限。也就是说散热或者功耗墙在起作用。但故事没结束。如果只是撞功耗墙为什么重启进程就好了继续往下查发现是另一个原因叠加这个程序在运行过程中不断分配新内存虽然逻辑上释放了但分配器没有归还给操作系统导致工作集持续增长LLC 命中率从 85% 一路掉到 40%。频率下降和缓存失效两个因素叠加才造成了这个线性变慢的曲线。修法分两步。针对功耗墙这属于硬件限制能做的只有改善散热或者降低期望针对缓存失效把内存分配器换成了会主动归还内存的实现并且在批处理边界上显式触发一次整理。改完之后曲线变平了稳定在 31 秒左右。这个案例的教训是症状是单一的原因往往是复合的查到第一个原因就停下很容易做出错误的归因。5.2 案例二内存带宽上不去CPU 利用率却不满现象一个多线程数据处理任务理论带宽需求约 60GB/s实测只跑到 38GB/s同时 CPU 利用率卡在 55% 上不去。加线程数没用提高到物理核数的两倍还是一样。这次的排查路径走的是 Uncore 侧。先算理论值双通道 4800MT/s上限 76.8GB/s实测 38GB/s 只有一半明显不正常。然后用perf看 Uncore 事件发现内存控制器的读带宽计数和实际业务量对得上说明请求确实发出去了。那问题就在请求的路上。下一步看 NUMA 分布用numactl --hardware和每个线程的本地节点命中率。发现有一半的线程分配的内存落在了远端节点上因为分配策略是默认的本地优先但进程启动时所在的核和后来实际运行的核不在同一个节点。远端访问会走互连总线带宽受限于互连而不是内存控制器。修法就是显式绑定numactl --cpunodebind0 --membind0 ./app或者更细粒度地用--interleave交错分配让每个线程的访问都有一部分在本地。改完之后带宽从 38GB/s 涨到 62GB/sCPU 利用率也跟着上到了 88%。这个案例说明一件事带宽上不去不一定是物理限制很可能是流量走错了路。而流量路径这个问题在 Core 层面完全看不出来。5.3 案例三压测数字忽高忽低复现不了现象同一个压测脚本连续跑十次吞吐量在 8000 到 12000 QPS 之间随机波动没有任何规律。这种问题最让人头疼因为它意味着你无法验证任何优化是否有效。排查的第一步是消除变量。先把线程数固定、绑核、绑 NUMA跑十次波动范围收窄到 9000-11000但没消失。说明还有别的变量。继续查用turbostat记录每次运行时的频率曲线发现高频运行的那几次Bzy_MHz普遍更高而且波动更小。这就把矛头指向了调频策略。再往下看问题出在调频策略的响应速度上。默认的策略是按需调频它根据最近的负载历史决定频率。压测启动的瞬间负载突然拉满但策略需要几百毫秒才能把频率拉上去如果这期间恰好有另一个短暂的空闲窗口频率又会被拉下来。每次运行的时机不同落在这个上升沿的位置就不同于是数字随机波动。修复方式很直接把调频策略切成固定性能模式。cpupower frequency-set -g performance改完之后波动范围收窄到 9500-10200 QPS可以接受。代价是待机功耗上升但对压测环境来说这个代价可以忽略。这个案例我印象很深因为它教会我一件事在混合架构上做性能测试第一件事是把所有能自动变的东西固定住。频率、绑核、NUMA、中断亲和性全部固定之后你才能开始测真正想测的东西。否则你测到的只是各路自动策略的随机组合。后来我把这套流程固化成了一个测试前的检查脚本每次压测前先跑一遍把不确定因素全部消除。这个习惯让我的测试数据可信度提升了一个档次也避免了很多改了半天没效果的无效工作。6. 一份可以直接抄的调优清单与几个反直觉结论写到这里把可操作的东西收敛成一份清单你照着检查一遍就行不用每次都从头推。第一步永远是定位分层。跑lscpu和turbostat看Bzy_MHz和UncMHzIPC 高不高LLC 未命中率高不高。这一步决定后面往哪个方向使劲。第二步摸拓扑用lscpu -e和thread_siblings_list把 P 核、E 核、超线程兄弟对、E 核簇的划分全部列出来写在纸上。第三步改配置按负载类型选方案延迟敏感就绑 P 核、避开兄弟对、绑本地节点、挪走中断吞吐型就全核开跑、主控放 P 核、工作线程按簇绑 E 核。第四步验证改一项测一项别一次改五个地方然后不知道是谁起的作用。几个我认为最值得记住的反直觉结论。E 核不一定更省电只有在多线程满载时才体现能效优势单任务放上去反而更耗电。Uncore 频率的影响远大于多数人的预期内存密集型负载下调整它能带来十几个百分点的差异而 Core 频率往往已经撞墙了。绑核是控制变量的手段不是提速的手段用来消除波动有效用来提升峰值通常无效。IPC 是比频率更可靠的性能指标频率可以做得很漂亮但 IPC 骗不了人。最后分享一个我一直在用的小技巧给机器建立一份性能基线档案。在干净的、空载的状态下跑一遍标准负载可以是固定的编译任务、固定的数据处理脚本记录下耗时、IPC、内存带宽、各层缓存命中率。以后任何一次性能异常都拿这份基线做对比能在一分钟内判断出是哪一层退化了。这份档案我建议跟着机器走而不是跟着人走因为基线的价值恰恰在于它的稳定性和可比性。真正把 Core 和 Uncore 分开看之后我最大的收获不是学会了调几个参数而是形成了一个条件反射看到任何性能问题先问这是执行的问题还是供给的问题。这个二分法几乎适用于所有场景包括那些看起来跟 CPU 完全无关的场景。想清楚这一层剩下的都是查文档和试参数的体力活。