一台 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 —— 仪器选错就会得出相反结论。


三、总:总结

技术结论

  1. 症状有两层,必须分开治。 软件层(Books 索引、fseventsd、内存枯竭、死配置)贡献了”点什么都没反应”的观感,修复后 load ↓94%、idle ↑5.3 倍;硬件/固件层的频率钳位贡献了”做什么都慢半拍”,修复后单核算力 ≈1.9×。只做前者,机器仍然慢;只做后者,机器仍然会卡死在索引风暴里。
  2. 钳位的真正机制是 XCPM 的升频策略,不是散热、不是供电、不是 PM 数据、不是 SMC 闩锁。 判据是:OS 侧 P-state 表被截断到 1.3 GHz,而 AppleXcpmForceBoost 一开就能跑到 2.5 GHz —— 说明硬件档位一直都在,只是系统不许用。
  3. 这台机器上的 CPU_Speed_Limit、hard_plimit_max_100mhz_ratio、cpu_thermal_level 三个字段都不可作为判据。 前两个在修复前后读数完全相同,第三个的 255 是”无热压力”的正常值。

方法论教训(每一条都对应我实际犯的错)

  1. 不要用没有标定的仪器下结论。 Python 循环、-O0 的 C 循环都因未考虑额外依赖路径而给出错误频率,直接导致我两次自相矛盾。选仪器要问:”它是否由架构定义?是否需要基准值?”
  2. “没变化”有两种完全相反的含义。 我把”电池与插电表现一致”读成”限流是假的”;它同样(甚至更典型地)意味着”钳位被锁死”。同一份数据支持相反结论时,先分辨机制,再选结论。
  3. 横向对照是廉价的证伪工具。 一句”拿同目录 76 个机型的 plist 比一下”就推翻了我”Apple 数据坏了”的判断。有对照样本时,永远先做对照。
  4. 用户的体感是数据,而且优先级高于我的推断。 “mini 比 MacBook 快很多”这句话的价值高于我当时整套分析,正是它把调查从”软件优化”推向”频率钳位”。
  5. 失败的实验要设计成能排除一整类原因。 CPUFriend 没解决问题,但它一次性排除了 PM 数据层,把 9 条假设压缩到 2 条。
  6. 改动启动分区前,必须验证备份能真的还原。 “存在一个 300 MB 文件” ≠ “可还原”;挂载镜像并比对 md5 才叫回滚点。写配置也要做等价性校验,不只是 lint。
  7. 仪器本身会失效,要知道它的失效条件。 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”:

  1. ⌥ Option 原生启动对照(不经过 OpenCore),确认表截断是否由引导链引入;
  2. 升级 OpenCore + 重打 OCLP 根补丁,或干净重装 macOS 12.7.x —— 注意 boot-args 里的 -nokcmismatchpanic 说明内核一直在容忍一次组件版本不匹配,这正是 P-state 表被截断的可疑来源。

回滚随时可做:该键改回 False,或还原 config.plist.bak / 整分区镜像。

最后一句

这台机器最终没有被换掉,也没有送修。一行配置,把一台被锁在 43% 性能的十年老机器救回来了。 但真正值得记住的不是这一行配置,而是:在得出”CPU_Speed_Limit = 34 是真的”这个正确结论之前,我用错了三次尺子;而在推翻它、又再次证明钳位真实存在的过程中,唯一始终可靠的锚点是**用户那句”mini 比它快很多”**。