一台 2015 年的 12 寸 MacBook,如何被锁在 43% 的性能上
排查窗口:2026-09-28 10:14 → 2026-09-29 00:17(约 13.5 小时,跨 3 次重启)
设备:MacBook (12 英寸, Early 2015) ·MacBook8,1· Core M-5Y71 · 8 GB · 512 GB PCIe SSD
系统:macOS 12.6.6 (21G646) · Darwin 21.6.0 · x86_64 · OpenCore Legacy Patcher 引导
复验:2026-11-01(修复一个月后)
一、总:背景与结论
起因
一台用了十年的 12 寸 MacBook,体感”越来越卡”。同一局域网里还有一台 2014 年的 Mac mini,配置看起来更弱(4 GB 内存、更老的 Haswell),但用起来明显比这台 MacBook 快。
这个反差很反直觉:MacBook 有 8 GB 内存、更快的 PCIe x4 SSD、更新的 Broadwell 架构、更高的 Retina 屏。按纸面参数,它不该输。
一句话结论
卡顿有两层原因:软件层的索引风暴与内存枯竭(已修复),以及一个更深的硬件级问题 —— CPU 被 XCPM 锁在约 1.0 GHz,而这颗芯片额定单核睿频是 2.6 GHz,实际只发挥出约 43% 的算力。
最终修复只用了一行配置:OpenCore 的
Kernel → Quirks → AppleXcpmForceBoost: False → True。
结果
| 指标 | 修复前 | 修复后 |
|---|---|---|
| C 依赖链基准(单核) | 1.00–1.07 GHz | 1.67 / 2.01 / 2.01 GHz |
powermetrics 实测频率 |
钉死 1000.02 MHz(标称的 76.92%) | 1620–1860 MHz,峰值采样 2105–2522 MHz(标称的 162%–194%) |
| Python 3M 循环(同一把尺子) | 1.248 s | 0.741–0.795 s(1.6–1.7×) |
| 满载包功耗 | 2.25–3.17 W(根本打不满) | 5.23–5.86 W |
| 软件层 load average | 53.84 | 3.49 |
代价:一台无风扇机身的满载稳态温度升到 95 °C(实测达到热平衡后稳定,未触发 PROCHOT,撤负载 60 秒回落到 62 °C)。
这次排查的成本
- 3 次重启、1 次 SMC 重置、1 次改动 EFI 启动分区(已完整回滚)
- 4 次推翻自己的结论(全部记录在下文,没有删)
- 9 条假设,8 条被证伪,1 条命中
二、分:过程
第 1 幕:体检 —— 一台”高负载低 CPU”的机器
第一轮采集就出现反常组合:
load averages: 57.62 28.46 30.21
CPU usage: 49.47% user, 38.67% sys, 11.84% idle
Processes: 367 total, 88 running, 1507 threads
PhysMem: 7955M used (1383M wired), 236M unused
先踩的第一个坑:macOS 的 load average 统计的是可运行线程数,不是进程数。4 逻辑核上看到 57,直觉会判成”卡在不可中断 I/O”;实测 U 状态进程数为 0,所以它真的是 CPU 排队 —— 而队列这么长的原因,后来才知道是 CPU 本身只有 1/3 速度。
同时发现这台机器是 OpenCore Legacy Patcher 改装系统:
kextstat 里的 6 个第三方 kext:
as.vit9696.Lilu 1.6.6 as.lvs1974.AirportBrcmFixup 2.1.7
com.dhinakg.AMFIPass 1.3.1 com.khronokernel.FeatureUnlock 1.1.4
com.khronokernel.RSRHelper 1.0.0 com.khronokernel.AutoPkgInstaller 1.0.1
/Users/Shared/.com.dortania.opencore-legacy-patcher.plist 存在
SIP: Custom Configuration(Kext Signing + Filesystem Protections 均 disabled)
boot-args: keepsyms=1 debug=0x100 ipc_control_port_options=0 -nokcmismatchpanic
⚠️ 这里有个容易犯的错:看到 SIP 半关就想”修复”它。实际上这是 OCLP 打根补丁的必要条件,执行
csrutil enable会让 Lilu 系 kext 无法加载,机器可能直接起不来。
第 2 幕:软件层优化 —— 有效,但不是主因
定位到的元凶(实测 CPU 占比):
| 元凶 | 占比 | 性质 |
|---|---|---|
Books + com.apple.BKAgentService |
50% + 33%(占 448 MB) | 正在猛写自己的 SQLite 库 —— 用户拿它打开了一批课程 PDF |
fseventsd |
47.4% | 文件事件风暴:坚果云 fsmonkd + iCloud bird/cloudd + Books 索引 + Downloads 里大批 PDF/mp3 |
WindowServer |
37% | 2304×1440 Retina 合成压在 HD 5300 上 |
com.apple.quicklook.ThumbnailsAgent |
186 MB | 卡死泄漏 |
.bash_profile 死 conda 块 |
— | 每个新 shell 都 fork+exec 一个不存在的 /opt/anaconda3/bin/conda |
处理:退出 Books、杀掉卡死的 agent、关闭透明/窗口动画、Finder 改列表视图、注释死配置、整盘关闭 Spotlight 索引(先确认该机器所有搜索类别本来就是 enabled = 0,索引建了也搜不到,属纯浪费)、hibernatemode 0 释放 1 GB sleepimage、关闭 standby/powernap/autopoweroff、清除 6 个 Docker 断链。
| 指标 | 优化前 | 优化后 |
|---|---|---|
| load average(1 分钟) | 53.84 | 3.49(↓94%) |
| CPU idle | 11.84% | 63.17%(↑5.3 倍) |
fseventsd |
47.4% | 0.0% |
WindowServer |
37% | 5.0% |
| swap 换页 | 累计 189 万 / 216 万 | 0 / 0 |
mds_stores |
运行中 | 全天未运行一次 |
| shell 启动 | 0.066 s | 0.027 s |
但用户仍然觉得慢。 这句话是整场排查的转折点。
第 3 幕:体感与数据冲突 —— 用户是对的
我给出的解释是”MacBook 日常交互更流畅(8 GB vs 4 GB),mini 只在持续算力上更强”。用户凭实际体感反驳:mini 快很多。
当实测数据与用户长期体感冲突时,应该先怀疑自己的推断,而不是先怀疑用户的感受。 于是我回去重测频率 —— 然后发现了一个一直被误解释掉的现象:
CPU_Speed_Limit = 34 连续 5 次采样完全一致
Thermal Warning Level = 0 Performance Warning Level = 0
从 10:52 到 23:49,横跨电池 / AC / 快充 / 涓流 / 4 线程满载 / 空载六种状态,这个 34 一次都没变过。
第 4 幕:找到正确的尺子(这里我错了三次)
要判断”CPU 是否被限流”,必须测出实际核心频率。三条路的可靠性差别巨大:
| 方法 | 结果 | 为什么不行 |
|---|---|---|
sysctl hw.cpufrequency |
恒为 1300000000 |
❌ 静态快照,且本机 min == 当前 == max,完全不反映实时 P-state |
Python for i in range(3000000): x+=i 反推 |
推出”约 0.44 GHz” | ❌ 需要预知”这台机器跑 CPython 该多快”作基准,而我的基准是按现代核心估的 |
C 语言 -O0 依赖链 |
0.51 GHz | ❌ -O0 下循环变量驻留内存,附带 load/store/cmp/branch 的额外依赖路径,严重低估 |
C 语言 -O2 + 每次迭代 32 条依赖 add |
1.08–1.10 GHz | ✅ add 依赖链每周期 1 条,由架构定义,不需要任何标定常数 |
核心代码(32 条依赖 add,把循环开销摊薄到不足 3%):
for (long i = 0; i < ITER; i++) {
__asm__ volatile(
"addq $1,%0\n\t addq $1,%0\n\t ... /* 共 32 条,全部依赖 %0 */"
: "+r"(a));
}
freq = (double)ITER * 32.0 / elapsed / 1e9;
最终用三个互相独立的仪器交叉印证,才敢下结论:
① C 依赖链基准 ≈ 1.08–1.10 GHz
② powermetrics 1000.02 MHz(4 个逻辑核全部一致,满载也不动)
③ machdep.xcpm hard_plimit_max_100mhz_ratio = 10
三者一致 → 结论成立。任何单一仪器都不够。
📌 补记(2026-11-01 复验时踩的坑):C 探针测的是墙钟时间,机器繁忙时会被抢占而严重低估 —— 复验当天 Chrome 把 load 压到 104,探针读出 0.46–0.90 GHz,而
powermetrics同期读 1523 MHz。探针必须在空闲机器上跑;判断”是否被限流”要看powermetrics的 P-state 占用率,它不受调度干扰。
第 5 幕:假设 - 证伪全表
| # | 假设 | 验证手段 | 结果 |
|---|---|---|---|
| 1 | 过热降频 | die 60–72 °C、Thermal pressure: Sleeping、Warning Level 0 |
❌ |
| 2 | BD PROCHOT 硬件保护 | powermetrics -s smc → Number of prochots: 0 |
❌ |
| 3 | 充电器功率预算不足 | 充电电流 2391→1355 mA(释放约 11 W)后算力毫无变化 | ❌ |
| 4 | 电池状态 / 电量 | 37% / 89% / 94% / 纯电池 四态钳位恒定 | ❌ |
| 5 | macOS 运行态问题 | 3 次重启,钳位始终存在 | ❌ |
| 6 | Apple PM 睿频数据选错/损坏 | CPUFriend 注入 Apple 原生 Vector#1,确认注入成功 | ❌ |
| 7 | SMC 状态闩锁 | 用户执行 SMC 重置(硬断电) | ❌ |
| 8 | XCPM 升频策略被卡住 | AppleXcpmForceBoost = True |
✅ 命中 |
| 9 | 硬件(SMC / PECI / 热敏走线) | — | 未触及(已被 8 解决) |
其中两条”看起来像证据”的东西,后来都被证明是误读:
cpu_thermal_level: 255—— 我一度读成”传感器坏了”。实际 XNU 里XCPM_THERMAL_LEVEL_INVALID = -1,按无符号字节打印就是 255,含义是**”当前没有热压力”,是健康值**。CPU_Speed_Limit = 34× 2.6 GHz = 0.884 GHz,与四核实测 0.88 GHz”吻合” —— 我拿它当作”限流为真”的证明。这是巧合,不是因果:修复后该字段仍然显示 34,而 CPU 已经跑在 2.5 GHz。它和hard_plimit_max_100mhz_ratio一样,在本机是装饰性读数。
第 6 幕:CPUFriend 实验 —— 最彻底的一次失败
CPUFriend(acidanthera)通过 hook X86PlatformPlugin.configResourceCallback() 整体替换 PM 资源数据。官方 Instructions.md 的约束决定了正确做法:
- 输入必须是
X86PlatformPlugin/Resources里的完整原生 plist + 修改,不能是裸的 FrequencyVectors; - 装进
/Library/Extensions需 LiluFriend 且被明确不推荐 → 必须走 OpenCore 注入。
解码本机原生 PM 数据(Mac-BE0E8AC46FE800CC.plist,39 KB):
CPUFloor = 1000 ← 与实测 1000.02 MHz 分毫不差
BoostLimit = 0
Frequencies = {2400:0, 2600:1, 2900:2} ← 按最大睿频选向量
Vector#0 偏移 0x0388 = 0x00
Vector#1 偏移 0x0388 = 0x1a (ratio 26 → 2.6 GHz) ★ 本机型该用的那组
Vector#2 偏移 0x0388 = 0x1d (ratio 29 → 2.9 GHz)
这里我犯了第三个错:最初把”Vector#0 的 0x0388 = 0”读成”Apple 数据坏了”。横向比对同目录 76 个机型 的原生 plist 后发现,绝大多数机型的 vector#0 在该偏移就是 0 —— 普遍正常现象,结论作废。
于是改用不发明任何数据的方案:把 Apple 为这块主板自己写的 Vector#1 复制给全部三个索引,使睿频表必然可用。
动启动分区前先建回滚点,并验证它真的能还原:
| 备份 | 验证方式 |
|---|---|
整 EFI 分区镜像 dd if=/dev/rdisk0s1(314,572,800 B) |
实际 hdiutil attach + 挂载过,内含 EFI/OC/config.plist,md5 与分区内原文件一致 |
config.plist.orig(30,507 B) |
md5 b33e3514…1467b6 |
分区内 config.plist.bak |
同上 md5 |
config.plist 修改做三重校验:plutil -lint OK / 除 Kernel>Add 外与原版解析后完全等价 / 原 9 项 kext 顺序内容未变。Provider kext 用官方 ResourceConverter.sh --kext 生成,并逐字节反解验证(kext 内 base64 → sha256 与修改后 plist 一致)。
结果:干净地证伪。
注入侧:kextstat 显示 org.acidanthera.driver.CPUFriend (1.3.0) 已加载(父依赖 Lilu)
ioreg 中 4 个 CPU 节点均带 cf-frequency-data → personality 匹配成功
限制侧:频率仍 1.06 GHz,一切读数不变
随后完整回滚并验证:config.plist md5 与原版逐位一致,kext 条目回到 9 项,重启后 kextstat 确认未加载。
这次”失败”是整场排查里最有价值的一步:它一次性排除了”PM 数据层”这一整类原因,把搜索空间压缩到 XCPM 策略本身 —— 直接指向了下一步。
第 7 幕:SMC 重置 —— 部分可证,部分不可证
SMC 不暴露”我被重置过”的标志位,所以无法直接确认。但 pmset -g log 的 ShutdownCause 行是 powerd 开机时读 SMC 关机原因寄存器后写入的:
| 开机时刻 | ShutdownCause 记录 | powerd 启动记录 |
|---|---|---|
| 10:29 | cause: 5 软件发起的关机 |
✅ |
| 23:11 | cause: 5 软件发起的关机 |
✅ |
| 23:26(SMC 重置后) | 无 | 无 |
前两次普通重启成对留下记录,这次两条都缺失 → 上次断电不是软件关机而是硬切断,且 SMC 关机原因寄存器读回为空(被清掉的表征)。流程走到了位,但”四键组合是否被同时识别”无法从软件判定(用户还反馈左 Shift 接触不良,这个变量无法排除)。
无论哪种解释,结论一致:重置后钳位依旧。
第 8 幕:真正的修复 —— 一行配置
排查到最后,config.plist 里有一个一直为 False 的键:
AppleCpuPmCfgLock = False
AppleXcpmCfgLock = False
AppleXcpmExtraMsrs = False
AppleXcpmForceBoost = False ← 就是它
OpenCore 的这个 quirk 专门处理”XCPM 不让 CPU 升频”的情况。而本机的症状恰好是:P-state 表在 OS 侧被截断到 1.3 GHz(hw.cpufrequency_min == max == 1300000000,额定睿频 2.6 GHz),且 ratio_changes_total 在 23 小时里只有 31 次(正常机器会频繁上下浮动)。
改动只碰一个布尔值,便于归因:
写入前 live / orig / bak 三方 md5 一致:b33e35145dbd52b82c939c4d745e9871
五重校验:lint OK | 顶层唯一差异分支 = Kernel | Quirks 中实际改动仅 1 项
kext 列表未变(仍 9 项)| boot-args 未变 | ACPI/PlatformInfo 未变
重启后:
C 依赖链基准 1.67 / 2.01 / 2.01 GHz (原 1.00–1.07)
powermetrics 1620–1860 MHz 持续,峰值 2105–2522 MHz(标称的 162%–194%)
Python 3M 循环 0.741 / 0.795 / 0.781 s (原 1.248 / 1.230)
包功耗 5.23–5.86 W (原 2.25–3.17 W)
ratio_changes_total = 0 (常驻最高档,不再需要调档)
睿频区间整个解锁了。之前不是”1.0 GHz 的芯片”,而是系统不许升档。
第 9 幕:热行为验证(无风扇机身,必须做)
2 分钟持续 4 线程满载,每 20 秒采样:
| 时间 | 20s | 40s | 60s | 80s | 100s | 120s |
|---|---|---|---|---|---|---|
| die 温度 | 94.94 | 95.35 | 95.45 | 95.39 | 95.34 | 95.23 °C |
| 频率 | 1860 | 1620 | 1755 | 1731 | 1800 | 1845 MHz |
| 包功耗 | 5.36 | 5.78 | 5.73 | 5.56 | 5.86 | 5.23 W |
| prochots | 0 | 0 | 0 | 0 | 0 | 0 |
- 温度在 95.2–95.5 °C 达到热平衡后稳定,没有失控攀升;
- 频率始终维持在 ≈1.8 GHz,没有塌陷;
- 全程
prochots = 0、无热告警 —— 也就是说不依赖保护机制兜底; - 撤负载 60 秒后回落到 62.19 °C,散热通路正常;
- 空闲时包功耗仅 1.95 W —— ForceBoost 不增加空闲发热。
95 °C 接近 Broadwell 的 Tjmax(约 105 °C),但这是平衡点而非攀升,且电池就在主板旁边(健康度 85%),所以长时间 4 线程满载仍建议交给有风扇的机器。
第 10 幕:一个月后的复验(2026-11-01)
config.plist → AppleXcpmForceBoost = true (仍然生效)
powermetrics → 1447–1523 MHz(标称的 111%–117%) (仍高于标称 1.3 GHz)
ratio_changes_total = 0
uptime 8 天(期间重启过,配置持久)
修复稳定保持。同一天也顺带验证了第 4 幕补记的那个教训:机器被 Chrome 压到 load 104 时,C 探针读到 0.46–0.90 GHz,而 powermetrics 同期读 1523 MHz —— 仪器选错就会得出相反结论。
三、总:总结
技术结论
- 症状有两层,必须分开治。 软件层(Books 索引、fseventsd、内存枯竭、死配置)贡献了”点什么都没反应”的观感,修复后 load ↓94%、idle ↑5.3 倍;硬件/固件层的频率钳位贡献了”做什么都慢半拍”,修复后单核算力 ≈1.9×。只做前者,机器仍然慢;只做后者,机器仍然会卡死在索引风暴里。
- 钳位的真正机制是 XCPM 的升频策略,不是散热、不是供电、不是 PM 数据、不是 SMC 闩锁。 判据是:OS 侧 P-state 表被截断到 1.3 GHz,而
AppleXcpmForceBoost一开就能跑到 2.5 GHz —— 说明硬件档位一直都在,只是系统不许用。 - 这台机器上的
CPU_Speed_Limit、hard_plimit_max_100mhz_ratio、cpu_thermal_level三个字段都不可作为判据。 前两个在修复前后读数完全相同,第三个的 255 是”无热压力”的正常值。
方法论教训(每一条都对应我实际犯的错)
- 不要用没有标定的仪器下结论。 Python 循环、
-O0的 C 循环都因未考虑额外依赖路径而给出错误频率,直接导致我两次自相矛盾。选仪器要问:”它是否由架构定义?是否需要基准值?” - “没变化”有两种完全相反的含义。 我把”电池与插电表现一致”读成”限流是假的”;它同样(甚至更典型地)意味着”钳位被锁死”。同一份数据支持相反结论时,先分辨机制,再选结论。
- 横向对照是廉价的证伪工具。 一句”拿同目录 76 个机型的 plist 比一下”就推翻了我”Apple 数据坏了”的判断。有对照样本时,永远先做对照。
- 用户的体感是数据,而且优先级高于我的推断。 “mini 比 MacBook 快很多”这句话的价值高于我当时整套分析,正是它把调查从”软件优化”推向”频率钳位”。
- 失败的实验要设计成能排除一整类原因。 CPUFriend 没解决问题,但它一次性排除了 PM 数据层,把 9 条假设压缩到 2 条。
- 改动启动分区前,必须验证备份能真的还原。 “存在一个 300 MB 文件” ≠ “可还原”;挂载镜像并比对 md5 才叫回滚点。写配置也要做等价性校验,不只是 lint。
- 仪器本身会失效,要知道它的失效条件。 C 探针在机器繁忙时低估;
powermetrics才是判限流的权威。两个仪器打架时,先问谁的前提被破坏了。
可复用清单
# 1) 权威频率读数(判限流用这个,不受调度干扰)
sudo powermetrics -n 2 -i 1500 -s cpu_power | grep -E 'fraction of nominal|package power'
# 2) SMC 侧读数(温度 / PROCHOT / Plimit)
sudo powermetrics -n 1 -i 1500 -s smc
# 3) XCPM 状态
sysctl -a | grep machdep.xcpm
# 4) 周期绑定的实测频率探针(必须在空闲机器上跑)
clang -O2 -o cpu-frequency-probe cpu-frequency-probe.c && ./cpu-frequency-probe
# 5) 关机原因寄存器(判断上次断电性质 / SMC 是否被重置)
pmset -g log | grep -E 'ShutdownCause|powerd process is started'
# 6) 修复(仅适用于 OCLP/OpenCore 引导的 Intel Mac)
# config.plist → Kernel → Quirks → AppleXcpmForceBoost = True,重启
遗留与更优雅的解法
AppleXcpmForceBoost 是”顶格常驻”的粗暴解法,代价是失去空闲降频、满载稳态 95 °C。更优雅的做法是让 OS 拿到完整的 P-state 表,从而同时拥有”空闲降到 400–800 MHz”和”负载睿频 2.6 GHz”:
⌥ Option原生启动对照(不经过 OpenCore),确认表截断是否由引导链引入;- 升级 OpenCore + 重打 OCLP 根补丁,或干净重装 macOS 12.7.x —— 注意 boot-args 里的
-nokcmismatchpanic说明内核一直在容忍一次组件版本不匹配,这正是 P-state 表被截断的可疑来源。
回滚随时可做:该键改回 False,或还原 config.plist.bak / 整分区镜像。
最后一句
这台机器最终没有被换掉,也没有送修。一行配置,把一台被锁在 43% 性能的十年老机器救回来了。 但真正值得记住的不是这一行配置,而是:在得出”CPU_Speed_Limit = 34 是真的”这个正确结论之前,我用错了三次尺子;而在推翻它、又再次证明钳位真实存在的过程中,唯一始终可靠的锚点是**用户那句”mini 比它快很多”**。