
第十三章 性能意识入门:你代码慢在哪?profiling 的工程化思路
-
- 0. 本章目标与适用场景
- 1. 性能优化的底层逻辑:先度量,再优化
- 2. 三类瓶颈:CPU / IO / 内存(先做分类)
-
- 2.1 快速判断:看现象
- 3. 最小 profiling 工具链(够用就行)
-
- 3.1 先用最朴素的 `time`
- 3.2 `cProfile`:找函数级热点(CPU)
- 3.3 `line_profiler`:精确到行(定位“那一行”)
- 3.4 `memory_profiler`:看内存峰值(爆内存必备)
- 4. 数据/AI工程最常见的性能坑(高频清单)
-
- 4.1 Pandas 的 `apply`:最经典的性能陷阱
- 4.2 `groupby` + `transform` / `merge`:隐性大杀器
- 4.3 Python 循环处理大数组:算法复杂度不变,再快也没用
- 4.4 IO:读 CSV 比读 Parquet 慢得多
- 5. 一个可落地的 profiling 框架:把性能当成回归测试
-
- 5.1 基线与对比:固定输入 + 固定指标
- 6. 优化策略的优先级:先做“收益最大”的
- 7. 小结:性能意识=可解释、可复现、可持续
- 你可以直接拿来用的行动清单
- 下一章:
你有没有这种体验:
- 同样一段数据处理脚本,在你电脑上 2 分钟,到了服务器上 30 分钟;
- 明明“只是多加了一个特征”,训练时间翻倍;
- 你以为瓶颈在模型,结果慢在 groupby、慢在 IO、慢在一个不经意的 apply。
数据分析与 AI 工程里,性能问题往往不是“写得不够快”,而是没有性能意识: 你不知道时间花在哪里,于是你只能靠猜。
本章目标很明确: 建立一套 profiling 的思维框架,让你每次遇到“变慢”,都能快速定位:慢在 CPU?慢在 IO?慢在内存?慢在算法复杂度?慢在数据结构?
0. 本章目标与适用场景
学完你应该能做到:
1. 性能优化的底层逻辑:先度量,再优化
如果只记一句话:
Never optimize what you haven’t measured.
你需要把性能问题当成一个实验问题:
#mermaid-svg-57Ex7krZzG6ecLW0{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-57Ex7krZzG6ecLW0 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-57Ex7krZzG6ecLW0 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-57Ex7krZzG6ecLW0 .error-icon{fill:#552222;}#mermaid-svg-57Ex7krZzG6ecLW0 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-57Ex7krZzG6ecLW0 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-57Ex7krZzG6ecLW0 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-57Ex7krZzG6ecLW0 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-57Ex7krZzG6ecLW0 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-57Ex7krZzG6ecLW0 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-57Ex7krZzG6ecLW0 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-57Ex7krZzG6ecLW0 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-57Ex7krZzG6ecLW0 .marker.cross{stroke:#333333;}#mermaid-svg-57Ex7krZzG6ecLW0 svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-57Ex7krZzG6ecLW0 p{margin:0;}#mermaid-svg-57Ex7krZzG6ecLW0 .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-57Ex7krZzG6ecLW0 .cluster-label text{fill:#333;}#mermaid-svg-57Ex7krZzG6ecLW0 .cluster-label span{color:#333;}#mermaid-svg-57Ex7krZzG6ecLW0 .cluster-label span p{background-color:transparent;}#mermaid-svg-57Ex7krZzG6ecLW0 .label text,#mermaid-svg-57Ex7krZzG6ecLW0 span{fill:#333;color:#333;}#mermaid-svg-57Ex7krZzG6ecLW0 .node rect,#mermaid-svg-57Ex7krZzG6ecLW0 .node circle,#mermaid-svg-57Ex7krZzG6ecLW0 .node ellipse,#mermaid-svg-57Ex7krZzG6ecLW0 .node polygon,#mermaid-svg-57Ex7krZzG6ecLW0 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-57Ex7krZzG6ecLW0 .rough-node .label text,#mermaid-svg-57Ex7krZzG6ecLW0 .node .label text,#mermaid-svg-57Ex7krZzG6ecLW0 .image-shape .label,#mermaid-svg-57Ex7krZzG6ecLW0 .icon-shape .label{text-anchor:middle;}#mermaid-svg-57Ex7krZzG6ecLW0 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-57Ex7krZzG6ecLW0 .rough-node .label,#mermaid-svg-57Ex7krZzG6ecLW0 .node .label,#mermaid-svg-57Ex7krZzG6ecLW0 .image-shape .label,#mermaid-svg-57Ex7krZzG6ecLW0 .icon-shape .label{text-align:center;}#mermaid-svg-57Ex7krZzG6ecLW0 .node.clickable{cursor:pointer;}#mermaid-svg-57Ex7krZzG6ecLW0 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-57Ex7krZzG6ecLW0 .arrowheadPath{fill:#333333;}#mermaid-svg-57Ex7krZzG6ecLW0 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-57Ex7krZzG6ecLW0 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-57Ex7krZzG6ecLW0 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-57Ex7krZzG6ecLW0 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-57Ex7krZzG6ecLW0 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-57Ex7krZzG6ecLW0 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-57Ex7krZzG6ecLW0 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-57Ex7krZzG6ecLW0 .cluster text{fill:#333;}#mermaid-svg-57Ex7krZzG6ecLW0 .cluster span{color:#333;}#mermaid-svg-57Ex7krZzG6ecLW0 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-57Ex7krZzG6ecLW0 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-57Ex7krZzG6ecLW0 rect.text{fill:none;stroke-width:0;}#mermaid-svg-57Ex7krZzG6ecLW0 .icon-shape,#mermaid-svg-57Ex7krZzG6ecLW0 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-57Ex7krZzG6ecLW0 .icon-shape p,#mermaid-svg-57Ex7krZzG6ecLW0 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-57Ex7krZzG6ecLW0 .icon-shape rect,#mermaid-svg-57Ex7krZzG6ecLW0 .image-shape rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-57Ex7krZzG6ecLW0 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-57Ex7krZzG6ecLW0 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-57Ex7krZzG6ecLW0 :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
现象:变慢/超时/爆内存
定义指标:耗时/内存/P95
建立基线:固定输入+可复现
Profiling 找热点
提出假设:IO/CPU/算法/数据结构
小步优化:一次只改一件事
回归验证:与基线对比
记录结论:写入README/Runbook
这套流程能避免你在“拍脑袋优化”里浪费时间。
2. 三类瓶颈:CPU / IO / 内存(先做分类)
2.1 快速判断:看现象
- CPU-bound(算不动):CPU 长期 100%,风扇起飞;加线程不见得快
- IO-bound(读写慢):CPU 很闲,但一直在等磁盘/网络;加缓存可能立竿见影
- Memory-bound(内存/GC):内存飙升、swap、频繁 GC、甚至 OOM
你可以把“总耗时”粗略拆成:

这不是严格公式,但足够指导你先从哪类工具入手。
3. 最小 profiling 工具链(够用就行)
3.1 先用最朴素的 time
很多时候你只需要知道“哪一步最慢”。
import time
t0 = time.perf_counter()
# step 1
t1 = time.perf_counter()
# step 2
t2 = time.perf_counter()
print("step1:", t1 – t0)
print("step2:", t2 – t1)
print("total:", t2 – t0)
工程建议:把 pipeline 拆成 5~10 个粗粒度步骤,先做“宏观定位”。
3.2 cProfile:找函数级热点(CPU)
适用:你不知道慢在哪个函数。
python -m cProfile -o out.prof your_script.py
再用 pstats 查看 Top N:
import pstats
p = pstats.Stats("out.prof")
p.sort_stats("cumtime").print_stats(20)
你要关注两个指标:
- tottime:函数自身耗时
- cumtime:包含子调用的累计耗时
很多数据工程瓶颈,都会在 cumtime 里露头。
3.3 line_profiler:精确到行(定位“那一行”)
适用:你已经锁定函数,但想知道慢在函数内部哪里。
安装:
pip install line_profiler
写法:
from line_profiler import profile
@profile
def build_features(df):
df["a"] = df["x"].fillna(0)
df["b"] = df.groupby("user")["a"].transform("mean")
return df
运行:
kernprof -l -v your_script.py
你会得到逐行耗时,这对定位 Pandas 的“隐形慢点”非常有效。
3.4 memory_profiler:看内存峰值(爆内存必备)
安装:
pip install memory_profiler
写法:
from memory_profiler import profile
@profile
def load_big():
import pandas as pd
df = pd.read_parquet("big.parquet")
return df
运行:
python -m memory_profiler your_script.py
你会看到每行代码内存变化,常用于发现“复制太多”“中间变量太大”。
4. 数据/AI工程最常见的性能坑(高频清单)
下面这几类,基本占了 80% 的慢。
4.1 Pandas 的 apply:最经典的性能陷阱
很多人写:
df["y"] = df["text"].apply(lambda s: s.lower().strip())
这会把向量化计算退化成 Python 循环。
更好的方式(优先用向量化):
df["y"] = df["text"].str.lower().str.strip()
经验法则: 能用 str.*、dt.*、numpy 就不要 apply。
4.2 groupby + transform / merge:隐性大杀器
groupby、merge 很强,但很贵。你要关注:
- key 的基数(cardinality)是否过大
- 是否发生了意外的笛卡尔积
- 是否在循环里做 merge
建议先用 profiling + 样本数据估算复杂度。
4.3 Python 循环处理大数组:算法复杂度不变,再快也没用
当你对 N=1e7 的数据做 Python for 循环:
for x in arr:
...
你本质是在赌解释器速度。 如果复杂度是 (O(N)) 且 N 很大,你要尽快迁移到:
- numpy 向量化
- numba
- Cython
- 或下沉到数据库/分布式计算引擎
4.4 IO:读 CSV 比读 Parquet 慢得多
很多 pipeline 慢在“格式选择”:
- CSV:解析成本高、类型推断慢
- Parquet:列式存储,读取需要的列更快
- Feather/Arrow:中间缓存非常友好
工程建议:训练/特征中间层尽量用 Parquet,并且显式指定 dtype,减少推断。
5. 一个可落地的 profiling 框架:把性能当成回归测试
你写单元测试是为了防 bug;你写性能测试是为了防“悄悄变慢”。
5.1 基线与对比:固定输入 + 固定指标
建议给核心函数做一个最小性能基线:
- 输入:固定 1 万行样本(可脱敏)
- 指标:耗时、内存峰值、P95 延迟(按场景选)
#mermaid-svg-aLuvOG6U7XLls0Qu{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-aLuvOG6U7XLls0Qu .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-aLuvOG6U7XLls0Qu .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-aLuvOG6U7XLls0Qu .error-icon{fill:#552222;}#mermaid-svg-aLuvOG6U7XLls0Qu .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-aLuvOG6U7XLls0Qu .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-aLuvOG6U7XLls0Qu .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-aLuvOG6U7XLls0Qu .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-aLuvOG6U7XLls0Qu .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-aLuvOG6U7XLls0Qu .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-aLuvOG6U7XLls0Qu .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-aLuvOG6U7XLls0Qu .marker{fill:#333333;stroke:#333333;}#mermaid-svg-aLuvOG6U7XLls0Qu .marker.cross{stroke:#333333;}#mermaid-svg-aLuvOG6U7XLls0Qu svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-aLuvOG6U7XLls0Qu p{margin:0;}#mermaid-svg-aLuvOG6U7XLls0Qu .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-aLuvOG6U7XLls0Qu .cluster-label text{fill:#333;}#mermaid-svg-aLuvOG6U7XLls0Qu .cluster-label span{color:#333;}#mermaid-svg-aLuvOG6U7XLls0Qu .cluster-label span p{background-color:transparent;}#mermaid-svg-aLuvOG6U7XLls0Qu .label text,#mermaid-svg-aLuvOG6U7XLls0Qu span{fill:#333;color:#333;}#mermaid-svg-aLuvOG6U7XLls0Qu .node rect,#mermaid-svg-aLuvOG6U7XLls0Qu .node circle,#mermaid-svg-aLuvOG6U7XLls0Qu .node ellipse,#mermaid-svg-aLuvOG6U7XLls0Qu .node polygon,#mermaid-svg-aLuvOG6U7XLls0Qu .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-aLuvOG6U7XLls0Qu .rough-node .label text,#mermaid-svg-aLuvOG6U7XLls0Qu .node .label text,#mermaid-svg-aLuvOG6U7XLls0Qu .image-shape .label,#mermaid-svg-aLuvOG6U7XLls0Qu .icon-shape .label{text-anchor:middle;}#mermaid-svg-aLuvOG6U7XLls0Qu .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-aLuvOG6U7XLls0Qu .rough-node .label,#mermaid-svg-aLuvOG6U7XLls0Qu .node .label,#mermaid-svg-aLuvOG6U7XLls0Qu .image-shape .label,#mermaid-svg-aLuvOG6U7XLls0Qu .icon-shape .label{text-align:center;}#mermaid-svg-aLuvOG6U7XLls0Qu .node.clickable{cursor:pointer;}#mermaid-svg-aLuvOG6U7XLls0Qu .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-aLuvOG6U7XLls0Qu .arrowheadPath{fill:#333333;}#mermaid-svg-aLuvOG6U7XLls0Qu .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-aLuvOG6U7XLls0Qu .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-aLuvOG6U7XLls0Qu .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-aLuvOG6U7XLls0Qu .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-aLuvOG6U7XLls0Qu .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-aLuvOG6U7XLls0Qu .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-aLuvOG6U7XLls0Qu .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-aLuvOG6U7XLls0Qu .cluster text{fill:#333;}#mermaid-svg-aLuvOG6U7XLls0Qu .cluster span{color:#333;}#mermaid-svg-aLuvOG6U7XLls0Qu div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-aLuvOG6U7XLls0Qu .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-aLuvOG6U7XLls0Qu rect.text{fill:none;stroke-width:0;}#mermaid-svg-aLuvOG6U7XLls0Qu .icon-shape,#mermaid-svg-aLuvOG6U7XLls0Qu .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-aLuvOG6U7XLls0Qu .icon-shape p,#mermaid-svg-aLuvOG6U7XLls0Qu .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-aLuvOG6U7XLls0Qu .icon-shape rect,#mermaid-svg-aLuvOG6U7XLls0Qu .image-shape rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-aLuvOG6U7XLls0Qu .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-aLuvOG6U7XLls0Qu .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-aLuvOG6U7XLls0Qu :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
是
否
固定样本数据
跑函数/模块
记录耗时/内存
保存基线
每次改动跑对比
超过阈值?
回滚或优化
合并
这会让你的项目从“功能能跑”升级到“性能可控”。
6. 优化策略的优先级:先做“收益最大”的
下面是我在数据/AI工程里常用的优化顺序:
为什么并行放最后? 因为你没定位热点,盲目并行只会把复杂度放大,并引入更多不可控因素。
7. 小结:性能意识=可解释、可复现、可持续
你不需要一开始就写出“极致快”的代码。 你需要的是一套方法,让你遇到性能问题时:
- 不猜
- 不拍脑袋
- 不靠运气
而是用 profiling 讲清楚:
“慢在这里,因为这个函数占了 60% cumtime; 我把它从 apply 改成向量化后,耗时从 12s 降到 2.8s; 回归数据集下指标不变,性能基线更新为 3s。”
这就是数据/AI工程里真正的“可维护”。
你可以直接拿来用的行动清单
如果你今天就要开始做 profiling,按这个顺序:
下一章:
《第14章 代码质量清单:从“能跑”到“可交付”》
网硕互联帮助中心



评论前必须登录!
注册