# 先测量，再让它变快

关于性能的速成课。20 个概念——Profiling、Latency、Throughput、Batching、Bottleneck、Amdahl's Law、Big-O、N+1 Query、Caching、Percentiles、Tail Latency、Premature Optimization 等等——每个都有它的英文名、一张图和一张卡片：是什么 → 怎么运作 → 例子 → 陷阱。结尾附一张速查表。核心想法：对慢的直觉通常是错的，所以去 profile，修掉那一个占大头的东西，然后再测一遍。

- **Author:** Fedor Chishchin
- **Level:** intermediate
- **Reading time:** ~35 分钟阅读
- **Published:** 2026-06-13
- **Source:** https://fedorthinks.com/zh/learn/performance-express

---

## 1. 去测量，别去猜

性能的第一条规则，恰恰是人人都违反的那条：别相信你对「哪里慢」的直觉。几乎所有白费的优化，都来自跳过了这一步。

### 你对慢的直觉通常是错的

> 一个医生凭猜测就开出手术方案，从没做过检查——自信、迅速，却常常在错误的地方动刀。

开发者在「时间花到哪儿去了」这件事上，出了名地爱猜错。你确信很慢的那部分，往往没问题；真正的元凶在你从没怀疑过的地方——一个被调用了上百万次的小函数，一次藏起来的数据库调用。

凭直觉行动，意味着你优化了错的东西，添了复杂度，而程序依旧很慢。第一步从来不是「修它」——而是**弄清楚时间到底花在了哪儿。**

### 地图：七组共 20 个概念

> 一张地铁线路图：先看见完整的线路，再一站一站地坐过去。

这门课的概念分成七组——每章一组。先讲怎么测量。再讲你要的是哪种「快」。然后是 bottleneck 和对付它的经典办法。最后讲如何不被平均值骗，什么时候不该优化，以及怎么把整件事当作一个循环来跑。

_Diagram: 七组概念——课程的各章就按它们来排。_

### 怎么读这门课

> 修车师傅不会随手乱换零件：先上仪器，再修一处，再复测。

每个概念都享受同样的待遇：它的**英文名**（记住它——文档、profiler 和仪表盘里用的就是这个词）、一幅日常生活的画面、一张**图**，以及一张**卡片**，上面有*是什么*、_怎么运作_、_例子_、*陷阱*和*关联*。

大想法：性能不是一袋小技巧，而是一套方法。**测量 → 找到 bottleneck → 只修那一个 → 再测一遍 → 达到目标就停。** 课里其余的一切，都是这个循环里的细节。

### Profiling — 找出真正的开销

> 一张逐项列出的账单，一行一行地告诉你钱到底花在了哪儿——于是你不再瞎猜，而是看到吃掉预算的是那一笔，而不是你以为的那些。

**Profiler** 是一种工具，它测量你的程序到底把时间花在哪儿，细到每个函数。它把「我觉得这个慢」变成「这一个函数占了 80% 的运行时间」。有了它，你修的是真正要紧的东西，而不是你想象出来的东西。

不管是 profiler、计时日志，还是你的**可观测性（observability）**指标，原则都一样：**让测量，而不是直觉，把你指向问题。**

_Diagram: Profile 显示每个函数占的时间份额。你怀疑的是渲染（8%）；一条数据库查询就吃掉了 80%。_

#### Profiling (profiler)

- **是什么:** 测量程序到底把时间花在哪儿，细到每个函数。
- **怎么运作:** Profiler（或计时日志、指标）记录每个部分的耗时，并给出各自的占比。
- **例子:** 「`getOrders()` 占了请求的 80%」，而不是「渲染肯定慢」。
- **陷阱:** 在玩具数据上做的 profile 可能指错方向——要在贴近真实的负载下测量。
- **关联:** **Measure–Fix–Measure** 的第一步；正是它找出 **Bottleneck**。

### Measure–Fix–Measure — 测量、修复、再测量

> 科学家只改一个变量，重做实验，再检查结果——从不假定改动有效，总是确认它真的有效。

性能是一个循环：先测量找到慢的部分，改它，然后**再测一遍**，确认这次改动真的有帮助——而不是只把问题挪了个地方，或者让它更糟。

没有第二次测量，你就只是在猜自己到底有没有改善，而这正是「优化」悄悄让东西变慢的原因。把每一次修复都当作一个要验证的**假设**——和 AI 的 evals、代码的测试是同一种纪律。

_Diagram: 测量 → 只改一处 → 用同一个测试再测。更快了——保留；没更快——撤销。_

#### Measure–Fix–Measure (测量、修复、再测量)

- **是什么:** 每一次修复，都用前后两次测量来检验。
- **怎么运作:** 改动前后用同一个测试；一次只改一处，这样你才知道是什么起了作用。
- **例子:** 加了一个索引：查询从 400 ms 降到 40 ms——保留。没变化——撤销。
- **陷阱:** 没有第二次测量，一次「优化」可能悄悄拖慢系统，而没人察觉。
- **关联:** **Optimization Loop** 的核心；与 evals 和测试是近亲。

> **框定问题的那一问:** 「我真的测量过时间花在哪儿了吗——还是正要凭一个多半指错地方的直觉去优化某样东西？」

> 对慢的直觉通常是错的。先 profile，找出时间真正花在哪儿，修掉它，再测一遍确认——绝不凭猜测去优化。

## 2. Latency 与 Throughput

优化之前，先弄清你到底要的是哪一种「快」。两个不同的目标总是被混为一谈，而改善其中一个，对另一个可能毫无作用。

### Latency — 等一件事的时间

> 一杯咖啡从下单到端到手里要多久——一位顾客的体验，从头量到尾。

**Latency** 是一次操作从开始到结束要多久——一个请求、一次页面加载、一条查询。它是单个用户*感受到*的东西：等待。

当有人说「网站很慢」，他几乎总是指 latency——做了一件事到拿到响应之间的延迟。降低 latency，就是让每一件事本身都发生得更快。

_Diagram: Latency 是一个请求的整条路径：网络、服务器处理、网络返回、渲染。_

#### Latency (响应时间)

- **是什么:** 一次操作从开始到结束的时间，通常以毫秒计。
- **怎么运作:** 它把路径上的每一步加起来：网络、服务器处理、数据库、渲染。
- **例子:** 从点击到页面就绪——800 ms。
- **陷阱:** 单个 latency 数字通常是平均值，而平均值会藏住慢的情况（第 5 章）。
- **关联:** 与 **Throughput** 是不同的目标；用 **Percentiles** 来度量。

### Throughput — 每秒能处理多少

> 整家咖啡馆一小时能出多少杯咖啡——不是一杯要多久，而是整个店里的总流量。

**Throughput** 是系统在单位时间内处理多少工作——每秒多少个请求，每分钟多少个任务。它说的是总容量，而不是个体的等待。

一个系统可以有很棒的 throughput（同时服务成千上万人），而每个用户仍要等一会儿（latency 高）；也可以 latency 很低，但容量有限。它们是不同的目标，你追的是哪一个，决定了你该修什么。

_Diagram: Throughput 是系统的总流量：它每秒能处理多少个请求。_

#### Throughput (每秒请求数)

- **是什么:** 单位时间内完成的工作量：每秒请求数，每分钟任务数。
- **怎么运作:** 当系统并行处理得更多，或每个单位花得更少时，它就会上升。
- **例子:** 一个 API 每秒处理 1,000 个请求；一条夜间流水线每小时处理一百万行。
- **陷阱:** 高 throughput 不等于快的体验：每个用户仍可能等很久。
- **关联:** 常常通过 **Batching**，以牺牲 **Latency** 为代价来提高。

### Batching — 用 latency 换 throughput

> 摩托车把一个人送到最快（低 latency）；公交车一趟运的人最多（高 throughput）。哪种车最好，取决于你要什么。

Latency 和 throughput 是相互独立的，优化其中一个可能伤到另一个。**Batching**——把工作攒在一起处理——通常会提高 throughput，却增加 latency，因为每一项都要等那一批凑齐。

所以你得决定这里哪个更重要：面向用户的页面，生死取决于 latency；后台的数据流水线，在乎的是 throughput。说清你真正的目标，能让你不去优化那个对用户无关紧要的数字。

_Diagram: 一次一个——每一项立刻出发，但要跑很多趟。成批处理——只跑一趟，每秒处理得更多，但每一项都要等那一批。_

#### Batching (把工作攒成批)

- **是什么:** 把许多小操作收集成一个大操作。
- **怎么运作:** 固定开销（一次网络往返、启动一个任务）按每批付一次，而不是每一项付一次。
- **例子:** 用一条查询插入 1,000 行，而不是 1,000 条单独的查询。
- **陷阱:** 每一项都要等这一批凑满——latency 上升。
- **关联:** 用 **Latency** 换 **Throughput**；也是治好 **N+1 Query** 的同一个思路。

> **目标之问:** 「我是想让每一次操作感觉更快（latency），还是想处理更大的总量（throughput）？它们不是一回事，修好一个未必帮得上另一个。」

> Latency 是等一次操作的时间；throughput 是每秒能处理多少。它们是相互独立的目标——而 batching 往往是用 latency 换 throughput。

## 3. 找到 bottleneck

性能不是平均分布的。几乎总有一个部分占了大头——在你修好它之前，优化其他任何东西都是白费力气。

### Bottleneck — 一个慢的部分说了算

> 一条链子的强度，恰好等于它最弱的那一环——加固强的环毫无用处；能不能撑住，只由最弱的那一环决定。

在大多数慢的系统里，单独一个 **bottleneck** 就占掉了大部分时间——一条查询、一个慢服务、一个糟糕的循环。其余的部分本来就已经够快了。

这就是为什么猜测如此浪费：整门手艺就在于找到**占大头的开销**，而不是去改善那些本来就快的部分。

_Diagram: 请求要经过五个步骤，但 500 ms 里有 400 ms 是一条数据库查询。这就是 bottleneck。_

#### Bottleneck (占大头的开销)

- **是什么:** 占掉总时间大部分的那一个部分。
- **怎么运作:** Profile 显示各部分的占比；通常有一个部分远大于其余。
- **例子:** 一个页面要 500 ms，其中 400 ms 是对一个没建索引的列做的一条查询。
- **陷阱:** Bottleneck 很少出现在你预期的地方——所以要先 profile。
- **关联:** 用 **Profiling** 找到；**Amdahl's Law** 解释了为什么它是该修的东西。

### Limiting Step — 系统只能和它最慢的一步一样快

> 一条一路畅通的高速公路，直到某处封了一条车道，所有车都在龟速爬行——整趟路程的速度由那一个堵点决定，而不是那些畅通的路段。

一个要经过十个步骤的请求，受限于其中最慢的一步——即 **limiting step**。把另外九个快的步骤再提速，总时间几乎不动；慢的那一步决定了节奏。所以你要专门对准那个堵点。

还要注意：修好一个 bottleneck，常常会*暴露*出下一个：去掉最慢的一步，新的最慢一步就冒出来了。性能工作，就是不断找到并清除*此刻*正在限制系统的那个东西。

_Diagram: 修好了查询：500 → 140 ms。现在渲染是最慢的一步，下一轮就瞄准它。_

#### Limiting Step (下一个 bottleneck)

- **是什么:** 当前决定整条路径速度的那一步。
- **怎么运作:** 去掉一个 limiting step，次慢的那一步就成了新的 limiting step。
- **例子:** 查询快了 10 倍——现在 50 ms 的渲染成了最慢的一步。
- **陷阱:** Bottleneck 永远不会彻底消失，只会转移——这就是你需要一个目标来收手的原因。
- **关联:** 为什么 **Optimization Loop** 是个循环，以及为什么 **Performance Target** 是它的出口。

### Amdahl's Law — 去优化别处是白费力气

> 车子爆了胎，你却在给奖杯抛光——力气花在了毫无改变的地方，而真正拦住你的东西被晾在一边。

优化一个只占运行时间 2% 的部分，你最多只能赢得 2%。找到那个占 80% 的部分，修好它就能改变一切。这道算术叫作 **Amdahl 定律**：你的收益，被你所加速的那部分实际占的时间份额封顶。

把时间花在优化 bottleneck 以外的任何东西上，按定义就是帮不上多大忙的时间。它常常还会把事情变糟，为一个看不见的收益添上复杂度和 bug。测量让你不再满怀爱意地优化自己看得懂的那一部分，而是逼你去处理真正花钱的那一部分。**修好 bottleneck，别管其余**——直到其余的变成了 bottleneck。

_Diagram: 把渲染提速 2 倍——总时间减 5%。把查询提速 2 倍——总时间减 40%。收益被该部分的占比封顶。_

#### Amdahl's Law (收益被占比封顶)

- **是什么:** 给系统的某一部分提速，受限于那一部分所占的时间份额。
- **怎么运作:** 一个占 10% 时间的部分，即使被做到无限快，最多也只能省 10%。
- **例子:** 渲染（10%）快 2 倍：500 → 475 ms。查询（80%）快 2 倍：500 → 300 ms。
- **陷阱:** 优化自己看得懂的东西，感觉很好——但如果它不是 bottleneck，就没有收益。
- **关联:** **Bottleneck** 背后的数学；反对 **Premature Optimization** 的论据。

> **Bottleneck 之问:** 「这里到底是哪一个部分占了大头——我修的是它，还是在打磨一个本来就够快、几乎无关紧要的部分？」

> 通常是一个 bottleneck 占了大头的时间，而系统只能和它最慢的一步一样快。修好那一个；优化别的任何东西都是白费，直到它变成 bottleneck。

## 4. 最大的几类提速

等你真的找到了 bottleneck，修法通常是少数几个经典办法之一。就是这寥寥几个，贡献了现实中绝大多数的提速。

### Better Algorithm (Big-O) — 好算法胜过快机器

> 去同一座城市的两条路：一辆更快的车走又长又绕的路，仍然输给一辆更慢的车走笔直的捷径。路线比发动机更重要。

最大的提速，往往来自更好的**算法**或数据结构，而不是更快的硬件或微调。**Big-O** 描述输入变大时时间如何增长。把一个 **O(n²)** 的循环换成 **O(n)** 的哈希表查找，可以把几分钟变成几毫秒——再快的机器也比不上这样的改变。

在优化那些小东西之前，先问问是不是*思路*本身就错了。正如数据结构课所讲的，更好的形态是最便宜、也最大的提速。

_Diagram: 输入变大时，O(n²) 急剧蹿升；O(n) 平稳增长。数据一大，这就是几分钟与几毫秒的差别。_

#### Better Algorithm (Big-O，算法复杂度)

- **是什么:** 换一种思路，让工作量随数据增长得更慢。
- **怎么运作:** Big-O 显示增长的快慢：O(n²) 是嵌套的「每个跟每个」循环，O(n) 是一遍扫完。
- **例子:** 找出两个列表的公共项：嵌套循环 → 用哈希集合，一遍扫完。
- **陷阱:** 数据小的时候看不出差别——数据一大就爆炸。
- **关联:** **Performance by Design** 里的主要选择；见数据结构那门课。

### N+1 Query — 往返次数太多

> 为了买东西，一趟只买一样，跑了二十趟商店，而不是拿着清单去一次——吃掉你下午的是路上的奔波，而不是购物本身。

大量的慢，其实是**往返次数太多**——对数据库或服务反复调用，每次单独看都很便宜，成批累积起来却是灾难。

经典的就是 **N+1 查询**：先取一个列表，然后对每一项再多发一条查询，把一次往返变成了几百次。修法是**批量获取**——一次把所有东西都要来。减少往返的*次数*，通常胜过加快每一次，因为往返本身就是开销。

_Diagram: 左：1 条查询取列表 + N 条逐个查询。右：一条查询取回全部。_

#### N+1 Query (多余的往返)

- **是什么:** 先用一条查询取一个列表，再对每一项各发一条查询。
- **怎么运作:** 每趟去数据库都要付出网络和额外开销；N 趟就把这笔开销乘上 N。
- **例子:** 100 个订单 → 1 条查询取列表 + 100 条查询取客户。修法：一条带 `IN (…)` 的查询，或者一个 JOIN。
- **陷阱:** ORM 会把它藏起来：代码看起来是个简单的循环，底下却是几百条查询。
- **关联:** 用与 **Batching** 相同的思路治好；常见的 **Bottleneck**。

### Caching — 别把同一件事算两遍

> 每次有人点菜，都从头重新做一遍，而不是留一锅热着的——最快的活，就是不用重复的活。

**Caching**——把昂贵的结果记住，这样就不用重新计算——是最大的杠杆之一（它有自己专门的一门课）。

第一次请求走慢路径并把结果存下来；之后的请求直接从内存里拿。代价是内存，以及可能返回过期数据的风险。

_Diagram: 命中（hit）直接从内存回答。未命中（miss）走慢路径，并把结果记下来留给下次。_

#### Caching (记住昂贵的结果)

- **是什么:** 存下昂贵工作的结果，并再次提供出去。
- **怎么运作:** 查缓存：有——立刻返回（hit）；没有——计算、存下、返回（miss）。
- **例子:** 一个目录页要 300 ms 才能构建出来；从缓存里拿，只要几毫秒。
- **陷阱:** 缓存可能返回过期数据；什么时候清除它，本身就是个难题。
- **关联:** **Doing Less** 的一个特例；在 caching 那门课里有深入讲解。

### Doing Less — 最快的活，是你跳过的活

> 没人点的菜就不做，来了三个人就别摆二十个人的餐桌。

还有一个经典办法——**少做事**。**惰性（lazily）**地计算，只在真正需要时才算；避免加载你用不上的数据；跳过那些结果最后要被扔掉的工作。

最快的操作，就是你从不运行的那个。往往最好的优化不是把活干得更快，而是压根不干。

_Diagram: 全部工作里，真正需要的只是一部分。其余是没用到的数据、做得太早的工作，和被扔掉的结果。_

#### Doing Less (惰性求值（lazy evaluation）)

- **是什么:** 去掉工作，而不是给它提速。
- **怎么运作:** 惰性计算，只加载你需要的字段，丢掉没人用的结果。
- **例子:** 从数据库里只取你需要的 3 个字段，而不是全部 50 个；报表等到被打开时才生成。
- **陷阱:** 惰性的工作可能在之后、在一个尴尬的时刻咬你一口——要测量。
- **关联:** 包含 **Caching**（不重复），并治好一部分 **N+1 Query**。

> **提速之问:** 「这个 bottleneck 是算法不好、往返次数太多、缺少 caching，还是我本可以跳过的工作——也就是大多数真实提速背后的那几个办法？」

> 大多数提速就是那么几个经典办法：更好的算法、更少的往返（干掉 N+1）、缓存昂贵的结果，以及干脆少做事。最快的活，就是你跳过的活。

## 5. 平均值与那条尾巴

你衡量性能的方式，可能会对你撒谎。平均值是最让人安心、也最有误导性的数字，学会越过它去看，正是真正的性能工作与一厢情愿的区别。

### Average — 平均值会藏住慢的情况

> 一个房间里，九个人很舒服，一个人身上着了火，平均温度却很宜人——平均数抹掉了真正要紧的那种情况。

把性能报告成**平均值**（**mean**），危险地让人安心：它把快的大多数和慢的少数搅成一个皆大欢喜的数字。可用户感受的不是平均值——每个用户经历的是*他自己的*那个请求，慢的那些，每一毫秒都感受得到。

平均 200 ms 的响应时间，可能藏着这样一个事实：二十个用户里有一个要等五秒。平均值告诉你系统没问题，而你有真实的一部分用户正在受苦。

_Diagram: 十九个快的请求，一个要 5 秒的——而平均值那条线看起来依然平静。_

#### Average (Mean) (平均值陷阱)

- **是什么:** 各次耗时之和除以请求数。
- **怎么运作:** 它把快的和慢的请求混成一个数字。
- **例子:** 「平均是 200 ms」——而二十个请求里有一个要等 5 秒。
- **陷阱:** 看上去体面，而有些用户正在受苦。永远别把目标设在它上面。
- **关联:** 被 **Percentiles** 取代；它会藏住 **Tail Latency**。

### Percentiles — p50 与 p99 才显示真实体验

> 一份报告说的不只是典型的等待时间，还有「最慢的 1% 的人等了这么久」——把糟糕的体验点名说出来，而不是把它们平均掉。

**Percentile（百分位数）**抓住了平均值藏起来的东西。**p50**（中位数）是典型情况——一半的请求比它快。**p99** 是慢的尾部——99% 的请求比它快，所以它大致就是真实用户会碰上的最糟体验。

盯着 p99，而不只是平均值，你才能看到那些真正在受苦的用户。痛苦就住在尾巴里，而 percentile 就是让它看得见的办法。

_Diagram: 响应时间的分布：p50 落在快的那一大群里，p99 远在慢的尾部。_

#### Percentiles (p50 / p95 / p99)

- **是什么:** pN 是 N% 的请求都比它快的那个时间。p50 是中位数，p99 是尾部。
- **怎么运作:** 把响应时间排序，取正确位置上的那个值。
- **例子:** p50 = 120 ms（典型），p99 = 2 s（一百个请求里有一个要等两秒）。
- **陷阱:** 不能把各服务器的 percentile 取平均——要对所有请求一起计算。
- **关联:** 让 **Tail Latency** 看得见；**Performance Target** 就设在它们上面。

### Tail Latency — 用户记住的是什么

> 在一家餐厅吃到一顿糟糕的饭，在人的记忆里，比十几顿还不错的饭分量更重——塑造别人如何评判你的，是最糟的体验，而不是平均值。

那些慢的请求——也就是 **tail latency**——不成比例地塑造了用户对你产品的观感，因为糟糕的体验会让人记住。

而且在大规模下，尾巴影响的人比看上去的要多：如果每个页面都要发出许多调用，*至少有一个*慢的概率就会飞快上升，所以几乎每个用户最终都会感受到尾巴。这就是为什么认真的团队把目标设在 p99 上，而不是平均值：驯服最糟的那些情况，才让一个产品感觉上稳定地快。

_Diagram: 一个页面要等它的调用里最慢的那一个。100 次调用、其中 1% 偏慢时，约 63% 的页面会撞上尾巴。_

#### Tail Latency (慢的尾巴)

- **是什么:** 最慢的那些请求的耗时——p99 及更往后。
- **怎么运作:** 当一个响应由许多调用拼装而成，它要等其中最慢的那一个。
- **例子:** 每次调用只有 1% 的时候慢，可一个有 100 次调用的页面，约有 63% 的时候会撞上尾巴。
- **陷阱:** 每个响应里的调用越多，「罕见」的尾巴就越会变成常态。
- **关联:** 通过 **Percentiles** 看到；被 **Average** 藏住。

> **度量之问:** 「我是盯着平均值觉得没事——还是在看 p99，那里才会出现真正在受苦的用户？」

> 平均值会藏住用户真正感受到的慢。去看 percentile——p50 看典型，p99 看痛苦的尾巴——因为人们记住的是最糟的体验。

## 6. 过早优化

还有一种与无视性能相反的失败方式：太早、到处追着性能跑，还没弄清它是否要紧。关于它的那句名言，之所以出名，是有道理的。

### Premature Optimization — 万恶之源

> 为一场也许永远不会来的地震，把一座房子的每面墙都加固一遍，而这时还没有人搬进来——大把力气花在一个你还没有的问题上。

Donald Knuth 的那句名言——「**过早优化是万恶之源**」——警告的是：在你还不知道什么需要优化、或者到底有没有东西需要优化之前，就动手优化。

优化并不慢、甚至没怎么被运行的代码，什么也买不到，却花费不菲：复杂度、bug，以及从要紧事上偷走的时间。大多数代码不需要快；它需要正确而清晰。提速的工作，是留给测量证明为**热点（hot）**的那些部分的。

_Diagram: 十二个模块里，只有一个是热点——要提速的就是它。其余的保持正确而清晰。_

#### Premature Optimization (优化一个猜测)

- **是什么:** 在测量证明确有必要之前就去优化。
- **怎么运作:** 你用清晰度和时间，在一个没人需要速度的地方换来了速度。
- **例子:** 花一周时间给一个每月才打开一次的设置页做个精巧的缓存。
- **陷阱:** 当真正的 bottleneck 出现在别处时，纠缠成一团的「快」代码只会碍事。
- **关联:** 用 **Profiling** 治好；由 **Amdahl's Law** 解释；别与 **Performance by Design** 混淆。

### Correct → Clear → Fast — 先正确、清晰，在要紧处再求快

> 你把菜谱写得人人都能照着做，只把你一天做上一百次的那一步简化——不是每一步，只是热的那一步。

顺序是：先让它**正确**，再让它**清晰**，并且**只在测量显示要紧的地方才让它变快**。

清晰的代码，恰恰因为你看得懂，之后才更容易优化；当真正的 bottleneck 出现在别处时，纠缠成一团的「快」代码很难修。为速度而优化几乎总要牺牲可读性，所以你只在能换来真实、可测的收益的地方付出这个代价——其余的保持简单。

_Diagram: 步骤的顺序：先正确，再清晰，最后才求快——而且只在测量过的地方。_

#### Correct → Clear → Fast (先让它能跑，再让它对，再让它快)

- **是什么:** 一个优先级的顺序：正确性、清晰度，最后才是速度。
- **怎么运作:** 等清晰的代码已经有了、并且测量过，再在热点处精准地加入速度。
- **例子:** 先写一个朴素易读的 handler；profile 显示有一个热循环——只优化那一个。
- **陷阱:** 优化几乎总要牺牲可读性——只把它花在真实的收益上。
- **关联:** **Premature Optimization** 的解药。

### Performance by Design — 但在设计里也别无视性能

> 你不会给一间棚屋做抗震，但你也不会在沙地上盖摩天大楼——有些选择，早期做对很便宜，晚期再修则代价惨重。

这个警告并不是「别去想性能」。在 **O(n)** 同样容易的时候选了 **O(n²)** 的算法，或者选了一个撑不住规模的结构，是你日后要付大价钱的设计错误。

平衡点是：不要早早地做微观优化，但也别做出那种一开始就慢、日后又很贵才能撤销的架构选择。一开始就便宜地把**大的形状**做对；**细节**只在测量要求时才去优化。

_Diagram: 左——一开始就定下的决策：算法、数据结构、架构的形状。右——之后根据 profile 去调的部分。_

#### Performance by Design (一开始就把大的形状做对)

- **是什么:** 从一开始就带着对速度的考虑，来做关于系统形状的那些大决定。
- **怎么运作:** 算法、数据结构和架构在早期选定；微调留到后面，靠测量来做。
- **例子:** 从第一天起就选用带索引的查找，而不是扫描整张表——这不算过早。
- **陷阱:** 很容易躲在这条原则后面，去做对细节的普通过早优化。
- **关联:** **Premature Optimization** 的边界；它的主要工具是 **Better Algorithm**。

> **克制之问:** 「我优化这个，是因为测量显示它是真正的 bottleneck——还是只因为我能，拿清晰度去换一个没人需要的速度？」

> 过早优化是拿清晰度去换没人需要的速度。先让它正确而清晰，只在测量证明要紧的地方才求快——但也别选一个你日后会后悔的、天生就慢的形状。

## 7. 把它用好

做得好的性能工作，是一套平静、可重复的方法，而不是英雄式的手忙脚乱。整个实践可以装进一个短循环，而且只在确有理由时才去跑它。

### Optimization Loop — 测量，修 bottleneck，重复

> 修车师傅用仪器诊断，修那一个坏掉的零件，再复测——从不乱换零件，指望噪音自己消失。

方法简单而有纪律：**测量**以找到 bottleneck，**修**那一个，**再测一遍**来确认并暴露出下一个 bottleneck，**够快了就停**。

每一轮都瞄准当前占大头的开销，无视其余的一切。这个循环让性能工作保持诚实而高效——你总知道自己在做的是真正要紧的事，也总知道自己有没有帮上忙。

_Diagram: 测量 → 找到 bottleneck → 修那一个 → 再测量。达到目标——停；还没到——再来一轮。_

#### Optimization Loop (性能循环)

- **是什么:** 一个反复进行的周期：测量、找出、修一处、检查。
- **怎么运作:** 每一轮都是针对当前 **Limiting Step** 的一次 **Measure–Fix–Measure**。
- **例子:** 第 1 轮：给查询加个索引。第 2 轮：现在渲染是 bottleneck——缓存模板。达到目标——停。
- **陷阱:** 没有目标，循环永远不会结束。
- **关联:** 把 **Profiling**、**Bottleneck** 和 **Performance Target** 串在一起。

### Performance Target — 定个目标，然后收手

> 你给房子做保温，做到够暖和就停——不会一直往里加保温层，加到没人感觉得到的地步。

性能没有天然的终点——你总能把什么东西再弄快一点点。所以要定一个 **performance target**（「页面在 p99 下低于 300 ms」），达到了就停。

没有目标，优化就会变成一个收益递减、没完没了的时间黑洞，复杂度也会为没人注意到的收益悄悄混进来。知道「够快」对你的用户、你的场景意味着什么，才让你做刚刚好的性能工作，然后去建点别的东西。

_Diagram: p99 随每一轮下降。曲线一低于目标——停；再往后只剩收益递减。_

#### Performance Target (性能预算)

- **是什么:** 一个具体的「够快」数字，绑定在某个 percentile 上。
- **怎么运作:** 只要指标还高于目标，优化循环就继续跑；一旦低于目标就停。
- **例子:** 「首页 p99 低于 300 ms。」
- **陷阱:** 把目标设在平均值上会骗到你——要设在 p99 上。
- **关联:** **Optimization Loop** 的出口；用 **Percentiles** 度量。

### 整体是怎么拼在一起的

> 整条路线在一张地图上：你从哪儿出发，经过哪些站，在哪儿下车。

这些概念拼成一幅图。**Profiling** 找到 **bottleneck**；目标——**latency** 还是 **throughput**——告诉你该修什么。Bottleneck 通常由四个经典办法之一治好：**更好的算法**、**更少的往返**、**缓存**，或者**少做事**。然后你再测一遍，按 **percentile**，对照一个**目标**，然后收手。而**过早优化**，就是你跳过测量时发生的事。

_Diagram: Profile → bottleneck → 经典办法之一 → 再测量 → p99 目标让循环停下。底部——没有测量时会发生什么。_

#### 优化之前

- **我测量过吗**——时间到底花在了哪儿，还是我在猜？
- **Latency 还是 throughput**——我实际需要的是哪种快？
- **Bottleneck 是什么**——那个占大头时间的部分是哪个？
- **它是个经典问题吗**——算法不好、往返太多、缺缓存、多余的工作？
- **我在盯 p99 吗**，而不只是会藏住慢尾巴的平均值？
- **有没有一个目标**可以朝它优化，好让我知道何时收手？

#### 你优化得好的迹象

- 你动手前**先测量**，动手后**再测量**。
- 你修的是 **bottleneck**，不去管那些本来就够快的部分。
- 你的收益来自**算法、更少的往返和 caching**——而不是微调。
- 你用 **p99** 来判断速度，而不是一个好听的平均值。
- 你朝着一个**目标**优化并收手，让其余部分保持**清晰而正确**。

## 速查表：全部 20 个概念

### 测量

- **Profiling** (profiler): 测量程序到底把时间花在哪儿，细到每个函数。
- **Measure–Fix–Measure** (再测一遍): 每次修复都是一个假设，要靠第二次测量来确认。

### 速度目标

- **Latency** (响应时间): 一次操作从开始到结束的时间——用户感受到的东西。
- **Throughput** (每秒请求数): 系统每秒处理多少工作。
- **Batching** (把工作攒成批): 把工作攒在一起：throughput 上去了，latency 也跟着上去。

### Bottleneck

- **Bottleneck** (占大头的开销): 占掉大部分时间的那一个部分。
- **Limiting Step** (下一个 bottleneck): 系统只能和它最慢的一步一样快；修好它，下一个就会冒出来。
- **Amdahl's Law** (被占比封顶): 你的收益，被加速部分所占的时间份额封顶。

### 最大的几类提速

- **Better Algorithm** (Big-O): 换思路：O(n²) → O(n)，胜过任何硬件。
- **N+1 Query** (多余的往返): 对列表的每一项各发一条查询；靠批量获取治好。
- **Caching** (记住结果): 记住昂贵的结果，不再重新计算。
- **Doing Less** (惰性求值): 跳过多余的工作：惰性计算，只取你需要的数据，不产生被扔掉的结果。

### 尾巴

- **Average (Mean)** (平均值陷阱): 一个平静的数字，藏住了慢的情况。
- **Percentiles** (p50 / p99): p50 是典型情况，p99 大致是真实用户会碰上的最糟情况。
- **Tail Latency** (慢的尾巴): 最慢的那些请求；人们会记住它们，而调用一多，几乎每个人都会撞上。

### 克制

- **Premature Optimization** (优化一个猜测): 在测量显示有需要之前就去提速。
- **Correct → Clear → Fast** (先能跑、再对、再快): 先正确，再清晰，只在热点处求快。
- **Performance by Design** (大的形状): 一开始就把大的形状（算法、结构、架构）做对。

### 实践

- **Optimization Loop** (循环): 测量 → 修一个 bottleneck → 再测量 → 重复。
- **Performance Target** (够快): 一个 p99 上的「够快」数字——循环在这里停下。

> 性能是一个平静的循环：测量，修那一个 bottleneck，再测量，达到目标就停。只在证明确有必要的地方用它，其余一切都保持清晰。

速成课完 · 7 章 · 20 个概念

接下来是练习：拿一样感觉慢的东西，别去猜，真的去 profile 它——找出那一个占大头的函数或查询，只修那个，把前后都测一遍。然后看它的 p99，而不只是平均值。当一次靠测量找到的、有针对性的修改，胜过一周的直觉所能换来的一切，这套方法就在那一刻自证了。但有一个想法要高过其余：先测量。几乎每一场性能灾难，都是有人在优化他猜出来的、而不是他测出来的东西——而几乎每一次真正的提速，都是找到那一个要紧的 bottleneck，并且恰好修了那个。
