fedorthinks

速成课 · No. 24

性能这件事,每次出错都是同一个套路:有人猜哪里慢,优化了错的地方,白白堆上了复杂度。该有的纪律恰好相反——靠测量找到真正的 bottleneck,只修那一个,然后再测一遍。搞清楚到底是什么让软件变慢,以及描述它的那些词,「让它更快」就从一句猜测变成了一套方法。

20 个概念 · 每个都有一张图 · 结尾附速查表

§ 01

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

你对慢的直觉通常是错的

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

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

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

地图:七组共 20 个概念

一张地铁线路图:先看见完整的线路,再一站一站地坐过去。

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

MeasureProfilingMeasure–fix–measureSpeed goalsLatencyThroughputBatchingBottleneckBottleneckLimiting stepAmdahl's lawBiggest winsBetter algorithmN+1 / round tripsCachingDoing lessThe tailAverage trapPercentilesTail latencyRestraintPremature optimizationCorrect → clear → fastPerf by designPracticeOptimization loopPerformance target
七组概念——课程的各章就按它们来排。

怎么读这门课

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

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

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

Profiling — 找出真正的开销

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

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

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

share of runtime, function by functiongetOrders()80%renderPage()8%parseJSON()5%formatDate()4%auth()3%← real cost← where you guessedmeasurement, not intuition, points at the problem
Profile 显示每个函数占的时间份额。你怀疑的是渲染(8%);一条数据库查询就吃掉了 80%。
ToolProfiling· profiler
是什么
测量程序到底把时间花在哪儿,细到每个函数。
怎么运作
Profiler(或计时日志、指标)记录每个部分的耗时,并给出各自的占比。
例子
「getOrders() 占了请求的 80%」,而不是「渲染肯定慢」。
陷阱
在玩具数据上做的 profile 可能指错方向——要在贴近真实的负载下测量。
关联
Measure–Fix–Measure 的第一步;正是它找出 Bottleneck。

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

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

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

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

Measurefind the slow partChangeone thingMeasure againsame testKeep itfaster ✓Revertsame or slowerevery fix is a hypothesis — confirm it with a second measurement
测量 → 只改一处 → 用同一个测试再测。更快了——保留;没更快——撤销。
PracticeMeasure–Fix–Measure· 测量、修复、再测量
是什么
每一次修复,都用前后两次测量来检验。
怎么运作
改动前后用同一个测试;一次只改一处,这样你才知道是什么起了作用。
例子
加了一个索引:查询从 400 ms 降到 40 ms——保留。没变化——撤销。
陷阱
没有第二次测量,一次「优化」可能悄悄拖慢系统,而没人察觉。
关联
Optimization Loop 的核心;与 evals 和测试是近亲。

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

§ 02

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

Latency — 等一件事的时间

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

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

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

latency = one operation, start to finishnetworkserver worknetworkrenderclickpage readywhat one user feels: the wait
Latency 是一个请求的整条路径:网络、服务器处理、网络返回、渲染。
MetricLatency· 响应时间
是什么
一次操作从开始到结束的时间,通常以毫秒计。
怎么运作
它把路径上的每一步加起来:网络、服务器处理、数据库、渲染。
例子
从点击到页面就绪——800 ms。
陷阱
单个 latency 数字通常是平均值,而平均值会藏住慢的情况(第 5 章)。
关联
与 Throughput 是不同的目标;用 Percentiles 来度量。

Throughput — 每秒能处理多少

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

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

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

throughput = how much work per secondSystem1,000 req / srequests inrequests donenot how long one takes — the total flow through the system
Throughput 是系统的总流量:它每秒能处理多少个请求。
MetricThroughput· 每秒请求数
是什么
单位时间内完成的工作量:每秒请求数,每分钟任务数。
怎么运作
当系统并行处理得更多,或每个单位花得更少时,它就会上升。
例子
一个 API 每秒处理 1,000 个请求;一条夜间流水线每小时处理一百万行。
陷阱
高 throughput 不等于快的体验:每个用户仍可能等很久。
关联
常常通过 Batching,以牺牲 Latency 为代价来提高。

Batching — 用 latency 换 throughput

摩托车把一个人送到最快(低 latency);公交车一趟运的人最多(高 throughput)。哪种车最好,取决于你要什么。

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

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

one at a time4 separate trips · each item leaves at onceServerbatched1 trip for all 4 · more per secondServeritems wait for the batchbatching: throughput ↑ · latency ↑
一次一个——每一项立刻出发,但要跑很多趟。成批处理——只跑一趟,每秒处理得更多,但每一项都要等那一批。
TechniqueBatching· 把工作攒成批
是什么
把许多小操作收集成一个大操作。
怎么运作

固定开销(一次网络往返、启动一个任务)按每批付一次,而不是每一项付一次。

例子
用一条查询插入 1,000 行,而不是 1,000 条单独的查询。
陷阱
每一项都要等这一批凑满——latency 上升。
关联
用 Latency 换 Throughput;也是治好 N+1 Query 的同一个思路。

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

§ 03

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

Bottleneck — 一个慢的部分说了算

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

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

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

Auth10 msLoad user15 msQuery orders400 msLogic25 msRender50 ms80% of the timetotal: 500 msone step dominates — that is the bottleneckthe other four together are 100 ms
请求要经过五个步骤,但 500 ms 里有 400 ms 是一条数据库查询。这就是 bottleneck。
ConceptBottleneck· 占大头的开销
是什么
占掉总时间大部分的那一个部分。
怎么运作
Profile 显示各部分的占比;通常有一个部分远大于其余。
例子
一个页面要 500 ms,其中 400 ms 是对一个没建索引的列做的一条查询。
陷阱
Bottleneck 很少出现在你预期的地方——所以要先 profile。
关联
用 Profiling 找到;Amdahl's Law 解释了为什么它是该修的东西。

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

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

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

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

beforeQuery 400 msRenderfix the query (400 → 40 ms)afterQueryRender500 → 140 msRender is now the slowest stepfix one bottleneck → the next one shows up
修好了查询:500 → 140 ms。现在渲染是最慢的一步,下一轮就瞄准它。
ConceptLimiting 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。

speed up Render 2× (10% of the time)Query475 ms · −5%speed up Query 2× (80% of the time)QueryRender300 ms · −40%the win is capped by the share of time that part takes
把渲染提速 2 倍——总时间减 5%。把查询提速 2 倍——总时间减 40%。收益被该部分的占比封顶。
LawAmdahl's Law· 收益被占比封顶
是什么
给系统的某一部分提速,受限于那一部分所占的时间份额。
怎么运作
一个占 10% 时间的部分,即使被做到无限快,最多也只能省 10%。
例子
渲染(10%)快 2 倍:500 → 475 ms。查询(80%)快 2 倍:500 → 300 ms。
陷阱
优化自己看得懂的东西,感觉很好——但如果它不是 bottleneck,就没有收益。
关联
Bottleneck 背后的数学;反对 Premature Optimization 的论据。

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

§ 04

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

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

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

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

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

input size n →timeO(n²) — nested loopO(n) — hash-map lookupminutes → milliseconds
输入变大时,O(n²) 急剧蹿升;O(n) 平稳增长。数据一大,这就是几分钟与几毫秒的差别。
TechniqueBetter Algorithm· Big-O,算法复杂度
是什么
换一种思路,让工作量随数据增长得更慢。
怎么运作

Big-O 显示增长的快慢:O(n²) 是嵌套的「每个跟每个」循环,O(n) 是一遍扫完。

例子
找出两个列表的公共项:嵌套循环 → 用哈希集合,一遍扫完。
陷阱
数据小的时候看不出差别——数据一大就爆炸。
关联
Performance by Design 里的主要选择;见数据结构那门课。

N+1 Query — 往返次数太多

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

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

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

N+1 queriesAppDBlistorder 1order 2order 3…order N1 + N round tripsBulk fetchAppDBall at onceall rows1 round trip
左:1 条查询取列表 + N 条逐个查询。右:一条查询取回全部。
Anti-patternN+1 Query· 多余的往返
是什么
先用一条查询取一个列表,再对每一项各发一条查询。
怎么运作
每趟去数据库都要付出网络和额外开销;N 趟就把这笔开销乘上 N。
例子

100 个订单 → 1 条查询取列表 + 100 条查询取客户。修法:一条带 IN (…) 的查询,或者一个 JOIN。

陷阱
ORM 会把它藏起来:代码看起来是个简单的循环,底下却是几百条查询。
关联
用与 Batching 相同的思路治好;常见的 Bottleneck。

Caching — 别把同一件事算两遍

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

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

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

RequestCacheseen it?Returnfrom memory · fastCompute / DBslow, expensivehitmissremember resultthe second ask is answered without redoing the work
命中(hit)直接从内存回答。未命中(miss)走慢路径,并把结果记下来留给下次。
TechniqueCaching· 记住昂贵的结果
是什么
存下昂贵工作的结果,并再次提供出去。
怎么运作
查缓存:有——立刻返回(hit);没有——计算、存下、返回(miss)。
例子
一个目录页要 300 ms 才能构建出来;从缓存里拿,只要几毫秒。
陷阱
缓存可能返回过期数据;什么时候清除它,本身就是个难题。
关联
Doing Less 的一个特例;在 caching 那门课里有深入讲解。

Doing Less — 最快的活,是你跳过的活

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

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

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

before: all the workneeded nowunused data ✗not needed yetthrown away ✗after: do lessneeded nowthe fastest work is the work you skipcompute lazily · don't load what you won't use · don't make what you'll discard
全部工作里,真正需要的只是一部分。其余是没用到的数据、做得太早的工作,和被扔掉的结果。
TechniqueDoing Less· 惰性求值(lazy evaluation)
是什么
去掉工作,而不是给它提速。
怎么运作
惰性计算,只加载你需要的字段,丢掉没人用的结果。
例子

从数据库里只取你需要的 3 个字段,而不是全部 50 个;报表等到被打开时才生成。

陷阱
惰性的工作可能在之后、在一个尴尬的时刻咬你一口——要测量。
关联
包含 Caching(不重复),并治好一部分 N+1 Query。

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

§ 05

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

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

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

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

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

average — looks fine1 in 20 waits 5 s20 requests
十九个快的请求,一个要 5 秒的——而平均值那条线看起来依然平静。
MetricAverage (Mean)· 平均值陷阱
是什么
各次耗时之和除以请求数。
怎么运作
它把快的和慢的请求混成一个数字。
例子
「平均是 200 ms」——而二十个请求里有一个要等 5 秒。
陷阱
看上去体面,而有些用户正在受苦。永远别把目标设在它上面。
关联
被 Percentiles 取代;它会藏住 Tail Latency。

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

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

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

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

p50 — half are fasterp99 — 99% are fasterthe slow taillatency →many requests are fast
响应时间的分布:p50 落在快的那一大群里,p99 远在慢的尾部。
MetricPercentiles· p50 / p95 / p99
是什么
pN 是 N% 的请求都比它快的那个时间。p50 是中位数,p99 是尾部。
怎么运作
把响应时间排序,取正确位置上的那个值。
例子
p50 = 120 ms(典型),p99 = 2 s(一百个请求里有一个要等两秒)。
陷阱
不能把各服务器的 percentile 取平均——要对所有请求一起计算。
关联
让 Tail Latency 看得见;Performance Target 就设在它们上面。

Tail Latency — 用户记住的是什么

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

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

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

Pageone user viewPage donewaits for the slowestcall 1 · 30 mscall 2 · 25 mscall 3 · 40 mscall 4 · 2 scall 5 · 35 ms100 calls × 1% slow each → ~63% of pages hit the tail
一个页面要等它的调用里最慢的那一个。100 次调用、其中 1% 偏慢时,约 63% 的页面会撞上尾巴。
ConceptTail Latency· 慢的尾巴
是什么
最慢的那些请求的耗时——p99 及更往后。
怎么运作
当一个响应由许多调用拼装而成,它要等其中最慢的那一个。
例子

每次调用只有 1% 的时候慢,可一个有 100 次调用的页面,约有 63% 的时候会撞上尾巴。

陷阱
每个响应里的调用越多,「罕见」的尾巴就越会变成常态。
关联
通过 Percentiles 看到;被 Average 藏住。

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

§ 06

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

Premature Optimization — 万恶之源

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

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

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

settingsadminexportsignupreportsemailscheckouthot · measured 70%searchbillingprofilelogsimportspeed work only where measurement shows heateverything else: keep it correct and clear
十二个模块里,只有一个是热点——要提速的就是它。其余的保持正确而清晰。
Anti-patternPremature Optimization· 优化一个猜测
是什么
在测量证明确有必要之前就去优化。
怎么运作
你用清晰度和时间,在一个没人需要速度的地方换来了速度。
例子
花一周时间给一个每月才打开一次的设置页做个精巧的缓存。
陷阱
当真正的 bottleneck 出现在别处时,纠缠成一团的「快」代码只会碍事。
关联

用 Profiling 治好;由 Amdahl's Law 解释;别与 Performance by Design 混淆。

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

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

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

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

make it work → make it right → make it fast1. Correctdoes the right thing2. Cleareasy to read and change3. Fastonly where measured hotclear code is easier to speed up later — tangled "fast" code is not
步骤的顺序:先正确,再清晰,最后才求快——而且只在测量过的地方。
PrincipleCorrect → Clear → Fast· 先让它能跑,再让它对,再让它快
是什么
一个优先级的顺序:正确性、清晰度,最后才是速度。
怎么运作
等清晰的代码已经有了、并且测量过,再在热点处精准地加入速度。
例子
先写一个朴素易读的 handler;profile 显示有一个热循环——只优化那一个。
陷阱
优化几乎总要牺牲可读性——只把它花在真实的收益上。
关联
Premature Optimization 的解药。

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

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

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

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

Decide earlyTune later, if measuredO(n) vs O(n²) algorithmdata structure that scalesarchitecture shapemicro-tweakstuning a hot functionclever speed trickscheap now, ruinous to change lateronly when a profile asks for it
左——一开始就定下的决策:算法、数据结构、架构的形状。右——之后根据 profile 去调的部分。
PrinciplePerformance by Design· 一开始就把大的形状做对
是什么
从一开始就带着对速度的考虑,来做关于系统形状的那些大决定。
怎么运作
算法、数据结构和架构在早期选定;微调留到后面,靠测量来做。
例子
从第一天起就选用带索引的查找,而不是扫描整张表——这不算过早。
陷阱
很容易躲在这条原则后面,去做对细节的普通过早优化。
关联
Premature Optimization 的边界;它的主要工具是 Better Algorithm。

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

§ 07

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

Optimization Loop — 测量,修 bottleneck,重复

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

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

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

MeasureFind bottleneckFix that oneMeasure againStop ✓not yettarget met
测量 → 找到 bottleneck → 修那一个 → 再测量。达到目标——停;还没到——再来一轮。
PracticeOptimization Loop· 性能循环
是什么
一个反复进行的周期:测量、找出、修一处、检查。
怎么运作

每一轮都是针对当前 Limiting Step 的一次 Measure–Fix–Measure。

例子

第 1 轮:给查询加个索引。第 2 轮:现在渲染是 bottleneck——缓存模板。达到目标——停。

陷阱
没有目标,循环永远不会结束。
关联
把 Profiling、Bottleneck 和 Performance Target 串在一起。

Performance Target — 定个目标,然后收手

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

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

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

p99 latencyiterations →target: p99 under 300 msstop here ✓diminishing returns
p99 随每一轮下降。曲线一低于目标——停;再往后只剩收益递减。
PracticePerformance Target· 性能预算
是什么
一个具体的「够快」数字,绑定在某个 percentile 上。
怎么运作
只要指标还高于目标,优化循环就继续跑;一旦低于目标就停。
例子
「首页 p99 低于 300 ms。」
陷阱
把目标设在平均值上会骗到你——要设在 p99 上。
关联
Optimization Loop 的出口;用 Percentiles 度量。

整体是怎么拼在一起的

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

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

ProfilingBottleneckLatency / ThroughputfindsgoalBetter algorithmN+1 / round tripsCachingDoing lessmeasure againPercentiles / p99Perf targetOptimization loopset onstopsPremature optimization ✗skip measuring → optimise a guess, add complexity
Profile → bottleneck → 经典办法之一 → 再测量 → p99 目标让循环停下。底部——没有测量时会发生什么。
优化之前
  • 我测量过吗——时间到底花在了哪儿,还是我在猜?
  • Latency 还是 throughput——我实际需要的是哪种快?
  • Bottleneck 是什么——那个占大头时间的部分是哪个?
  • 它是个经典问题吗——算法不好、往返太多、缺缓存、多余的工作?
  • 我在盯 p99 吗,而不只是会藏住慢尾巴的平均值?
  • 有没有一个目标可以朝它优化,好让我知道何时收手?
你优化得好的迹象
  • 你动手前先测量,动手后再测量。
  • 你修的是 bottleneck,不去管那些本来就够快的部分。
  • 你的收益来自算法、更少的往返和 caching——而不是微调。
  • 你用 p99 来判断速度,而不是一个好听的平均值。
  • 你朝着一个目标优化并收手,让其余部分保持清晰而正确。

速查表:全部 20 个概念

测量

Profilingprofiler

测量程序到底把时间花在哪儿,细到每个函数。

Measure–Fix–Measure再测一遍

每次修复都是一个假设,要靠第二次测量来确认。

速度目标

Latency响应时间

一次操作从开始到结束的时间——用户感受到的东西。

Throughput每秒请求数

系统每秒处理多少工作。

Batching把工作攒成批

把工作攒在一起:throughput 上去了,latency 也跟着上去。

Bottleneck

Bottleneck占大头的开销

占掉大部分时间的那一个部分。

Limiting Step下一个 bottleneck

系统只能和它最慢的一步一样快;修好它,下一个就会冒出来。

Amdahl's Law被占比封顶

你的收益,被加速部分所占的时间份额封顶。

最大的几类提速

Better AlgorithmBig-O

换思路:O(n²) → O(n),胜过任何硬件。

N+1 Query多余的往返

对列表的每一项各发一条查询;靠批量获取治好。

Caching记住结果

记住昂贵的结果,不再重新计算。

Doing Less惰性求值

跳过多余的工作:惰性计算,只取你需要的数据,不产生被扔掉的结果。

尾巴

Average (Mean)平均值陷阱

一个平静的数字,藏住了慢的情况。

Percentilesp50 / 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,并且恰好修了那个。