何时使用
当已存在且能跑通的代码,时间或空间复杂度高于必要值时使用本技能重构:
- 嵌套循环、
O(n²)或更差的扫描、重复计算、冗余分配、内存膨胀。 - 症状描述:"大输入下很慢"、"超时"、"OOM/内存太多"、"降低复杂度"、"优化这个算法"。
- ORM 的 N+1 查询(Prisma、Drizzle、SQLAlchemy、Django、ActiveRecord)。
- 对独立项的
for循环里写await,导致串行延迟。
不该用的边界:
- 写新代码时的复杂度预防 —— 不是本技能职责(源技能体系交给
lemmaly)。 - 数学级优化(Bloom、HLL、FFT、JL 投影)—— 升级到
mathguard。 - 瓶颈是 I/O / 网络 / 外部服务延迟而非 CPU/内存 —— 去修 I/O 模式,本 playbook 无效。
- 启动期只跑一次、n 很小(n < ~100 且不在热路径)的代码 —— 过度优化只增风险无收益。
铁律
变换前后,现有测试必须都为绿;没有测试不准动手
若代码无测试,先写一个特征化测试(golden 输入 → 当前输出),再变换,再验证测试仍通过。跳过这步,优化可能悄悄改坏调用方 ——「更快但更错」远比「慢但对」更糟。
步骤
对每一处要优化的代码,按顺序执行:
- 写下当前与目标 Big-O(动手前)。一行:当前
time=O(?) / space=O(?);目标time=O(?) / space=O(?);主导输入维度(n 是什么、实际多大)。说不出当前 Big-O,说明还没读懂代码,继续读。 - 定位瓶颈行,不要猜。 指向负责主导项的确切行:嵌套循环?重复线性扫描?重算?热循环里的分配?修复就在那一行,不在别处。
- 每次只做一处变换,跑"验证-回滚-停止"循环:
- 从 playbook 里取恰好一个变换应用。
- 跑现有测试套件(或铁律要求你写的特征化测试)。
- 任一测试挂掉 → 立即回滚。不准改测试,不准绕过失败。
- 统计这块代码的连续回滚次数。连续 3 次回滚 → 停止优化:瓶颈错了、变换错了、或代码有你没建模的不变量。升级到
invariant-guard补写缺失契约,不准试第四次。 - 变换落地为绿后,才挑下一个。
- 精确保持语义。 降复杂度不得改变输出、顺序保证、稳定性或错误行为。若优化必须改语义(如输出无序),显式声明并确认可接受。
- 不准编造数字。 没测过不准写"快 10 倍""省 200MB"。写
<measured: TBD>或真去测。 - 变换落地后必须报告实测加速比。 绿了之后跑代表性基准(同输入、同机器、热缓存),一行附在 diff 上:
p50: 186 ms → 1.1 ms (169× faster, n=20,000, 200 samples)
无法测量(纯渐近、手头没有对应输入)就显式写:asymptotic only, no measurement — O(n²) → O(n)。绝不静默跳过。
指令
时间复杂度变换 playbook(按此顺序尝试)
| 坏味道 | 修复 | 典型收益 |
|---|---|---|
for x in A: if x in B,B 是 list/array |
B 一次性转 Set/Map |
O(n·m) → O(n+m) |
| 嵌套循环算配对/连接 | 按 key 做 hash-join,按查找字段建索引 | O(n·m) → O(n+m) |
循环内反复 .find/.indexOf/.includes |
循环外预建索引 Map<key,item> |
O(n²) → O(n) |
| 重复重算同一值 | 按输入 key memoize/缓存 | O(n·f(n)) → O(n+f(n)) |
| 循环内排序 | 循环外排一次 | O(n² log n) → O(n log n) |
| 反复线性扫 min/max/中位数 | 堆 / 有序结构 | O(n·k) → O(n log k) |
| 递归重算(朴素 Fibonacci 形状) | memoize 或改迭代 DP | 指数级 → O(n) |
| 循环内字符串拼接(部分语言) | builder / join / push 后 join |
O(n²) → O(n) |
| 循环内重复编译正则 | 循环外编译一次 | 常数因子,但很大 |
| 嵌套循环计数/分组 | 单遍 Counter/Map<k,count> |
O(n²) → O(n) |
| 滑动窗口写成嵌套循环 | 双指针 / 窗口和 | O(n²) → O(n) |
| 重复前缀和 | 预算前缀数组,O(1) 区间查询 | O(n·q) → O(n+q) |
| 区间两两距离/包含检查 | 排序 + 扫描线 | O(n²) → O(n log n) |
| 全排序取 Top-K | 大小为 K 的堆 | O(n log n) → O(n log k) |
await in for(独立项) |
Promise.all / 批量并发 |
墙钟 O(n·延迟) → O(延迟) |
| 循环内 ORM 查询(N+1) | IN (...) / select_related / 批量取 |
O(n) 往返 → O(1) |
空间复杂度变换
| 坏味道 | 修复 | 典型收益 |
|---|---|---|
| 仅为遍历而物化整个 list | 生成器 / 迭代器 / 流 | O(n) → O(1) |
大数据上链式 .map().filter().map() 建中间数组 |
单遍循环 / 惰性管道 | k·O(n) → O(n) |
| 缓存递归每个中间结果 | 滚动窗口(只留最近 k 个状态) | O(n) → O(k) |
| 只需计数却存 parents/visited | 仅 bitset / 计数器 | O(n) → O(1) |
| 复制输入以便修改 | 调用方允许时原地修改 | O(n) → O(1) |
| 处理前读入整个文件 | 按行 / 分块流式 | O(file) → O(chunk) |
| 循环内深拷贝保安全 | 拷一次 / 结构共享 / 不可变 | O(n·m) → O(n+m) |
| 闭包/监听器/缓存阻止 GC | 限界缓存(LRU)、移除监听、收紧闭包 | 无界 → 有界 |
| 从 DB 加载全量结果集 | 游标 / 分页 / 流式查询 | O(rows) → O(page) |
JSON.parse(JSON.stringify(x)) 克隆 |
structuredClone 或定向拷贝 |
去除 O(n) 开销与分配 |
当无法降低渐近 Big-O 时
有时 O(n log n) 就是下界。此时转常数因子优化,并显式声明「渐近下界已是 O(n log n),仅做常数因子优化」:
- 指针追逐结构换连续数组(缓存局部性)。
- 循环不变量外提。
- 热循环内避免分配(复用 buffer)。
- 数值计算用 typed array / 原生容器替代装箱对象。
- 批量化 syscall / I/O。
输出纪律(提议或应用优化时,按此顺序)
- 瓶颈 —— file:line + 一句原因。
- 当前复杂度 ——
time=O(?) / space=O(?)。 - 变换 —— playbook 里的名字(或描述新变换)。
- 新复杂度 ——
time=O(?) / space=O(?)。 - 语义风险 —— 调用方可能察觉的(顺序、稳定性、错误时机)。属实时 "None" 是合法答案,但要写出来。
- 实测加速 ——
before → after+N× faster(或asymptotic only)。 - diff。
1–6 缺任一项,优化就不可应用。
示例
瓶颈: getOrdersWithUsers() 在 1 万订单上跑 10 秒。原因:map 内 users.find(u => u.id === o.userId) → O(n·m)。
反例(改了语义还改了测试):
export function getOrdersWithUsers(orders, users) {
const userById = Object.fromEntries(users.map(u => [u.id, u]));
return orders
.map(o => ({ ...o, user: userById[o.userId] }))
.filter(o => o.user); // 悄悄丢弃了 user 已删除的订单
}
更快,但结果集变了。顺手"修"掉了校验旧行为的断言,绿着上线,两周后账单报表崩了。
正解(一处变换,语义不变):
// Bottleneck: orders.map → users.find (line 14)
// Current: time=O(n·m), space=O(1) → Target: time=O(n+m), space=O(m)
// Transformation: 循环外预建索引 Map<userId, User>
// Semantic risk: None —— 缺失 user 的订单仍输出 user: undefined,与原行为一致
// Reverts so far: 0
export function getOrdersWithUsers(orders, users) {
const userById = new Map(users.map(u => [u.id, u]));
return orders.map(o => ({ ...o, user: userById.get(o.userId) }));
}
一处变换,测试不动,跑测试:绿则发,红则回滚(不准改测试)。连续 3 次回滚后停手,加载 invariant-guard。
注意事项
红旗 —— 立即停:
- 没写当前 Big-O 就优化;只说"应该更快"却指不出具体瓶颈行。
- 验证任何一处前就叠加多个变换。
- 没测量也没渐近论证就声称加速。
- 靠悄悄改输出语义来降复杂度。
- 去改写启动时只跑一次、n=12 的代码。
常见借口 vs 现实:
- "我脑子里已经想好了,先贴 diff 再补标签" → 事后补的标签会撒谎,按 瓶颈→复杂度→变换→diff 顺序写。
- "语义风险是 None,跳过这步" → "None" 合法,但必须写出来,读者不知道你考虑过哪些保证。
- "三个变换一个 diff 搞定" → 叠加变换会藏回归,一次一个。
- "晚点再测" → "晚点"就是永远的
<measured: TBD>,要么现在测,要么只接受渐近论证。
完成前的核对清单:
- 变换前现有测试(或已写的特征化测试)为绿。
- 恰好应用了一处变换;变换后测试为绿。
- 没有为了通过而修改/弱化/跳过任何测试。
- diff/PR 里写明了当前与目标 Big-O、语义风险。
- 报告了实测加速比(或显式标
asymptotic only);有测量声明时附上测量命令。 - 本代码回滚次数 < 3。
任一框没勾 = 没做完,要么回滚要么补齐,不准上半验证的加速。
停止条件 / 局限: 渐近 Big-O 已达问题已知下界、输入可证明小且有界、优化会损害正确性/可读性而无实测收益时,停手。本技能仅处理单进程、渐近优化(常数因子是另一独立模式);分布式瓶颈(共识延迟、复制滞后、队列背压)与 I/O 密集型代码不在范围内;基准需作者自备代表性输入,本技能不替你跑。
互见
lemmaly—— 写新代码时的复杂度预防网关(重构既有代码用本技能,不用它)。invariant-guard—— 连续 3 次变换都让测试挂掉时的升级目标:缺的是契约而非优化。mathguard—— 触及经典下界、需近似或数学结构(Bloom/HLL/FFT/JL)才能再赢时升级。
采编自 sickn33/antigravity-awesome-skills(原作者 morsechimwai,Apache-2.0),MIT 收录。