>>>PyPathPython 学习站
首页›进阶篇›kp-034
进阶篇 · Advanced lv.3 进阶 kp-034

性能分析与优化

前置知识:kp-022、kp-033

1. 一句话定义

性能工程的第一定律是先测量后优化:timeit 测微基准,cProfile 找热点,优化顺序遵循"算法 → 数据结构 → Pythonic 内建 → 向量化/C 扩展 → 并行",最后才是考虑换语言。

2. 为什么重要

凭直觉优化是 Python 性能工作最大的时间黑洞:猜对率极低且常把代码改丑。标准库自带的专业工具链(timeit/cProfile/pstats/tracemalloc)能让每一步优化都有数据背书。

3. 前置知识

kp-022(标准库)、kp-033(内存)。

4. 核心概念

python
# ① 微基准:timeit(多次重复取均值,屏蔽环境噪声)
import timeit
timeit.timeit("'-'.join(str(n) for n in range(100))", number=10000)
timeit.timeit("'-'.join([str(n) for n in range(100)])", number=10000)  # 列表推导版

# ② 全程序剖析:cProfile + pstats
import cProfile, pstats
prof = cProfile.Profile()
prof.enable()
run_my_pipeline()
prof.disable()
stats = pstats.Stats(prof).sort_stats("cumulative")
stats.print_stats(10)     # 累计耗时前 10

# ③ 单行探针:perf_counter
from time import perf_counter
t0 = perf_counter(); work(); print(perf_counter() - t0)

5. 原理与机制

优化漏斗(按性价比排序):

text
1. 算法复杂度        O(n²) → O(n log n)     (收益数量级)
2. 数据结构选型      list 查找 → set/dict    (kp-010)
3. 减少工作          缓存 lru_cache、惰性生成器、避免重复 IO
4. 内建与推导式      map/sum/join/推导式(C 层循环)
5. 向量化            NumPy/Polars:把 Python 循环下沉到 C(kp-041)
6. 并行              多进程/线程(kp-030~032)
7. 编译/换语言       Cython、mypyc、PyO3(Rust)、PyPy(JIT)

Python 慢的机理(kp-001 的延伸):动态类型导致每条字节码都要做类型分派;对象在堆上、指针跳转多、CPU 缓存不友好。所以"把循环下沉到 C"几乎是所有 Python 生态性能库的共同思路。

6. 关键事实(模型/图示)

text
经验阈值(数量级参考,须自测):
  纯 Python 循环操作     ~0.1 µs/次
  dict/set 查找          ~0.05 µs(O(1))
  函数调用               ~0.05-0.1 µs
  C 扩展向量化运算        快 10-100×
规则:先用 profiler 找到 ≥10% 耗时的热点,再动手

7. 直观类比

性能优化像医生看病:cProfile 是"全身 CT"(哪里耗时最多),timeit 是"单项化验"(对比两种写法),优化手段是"治疗"——按 1 到 7 的顺序、从便宜到昂贵。直接上来吃猛药(换语言/上并行)等于不问诊先开刀。

8. 实例与案例

python
# 一个真实的优化剧本
# 症状:处理 10 万行 CSV 花 40s
# ① cProfile → 82% 耗时在 row_purpose(x) 内的 x in all_tags(list 扫描)
# ② 数据结构修复:all_tags: set → 40s → 1.2s
# ③ 剩余热点是逐行 json.loads → 换 orjson / 批处理 → 0.4s
# 全程没有并行、没有换语言

9. 常见误区

  1. 不测量直接优化 —— 优化不热的地方 = 零收益 + 可读性税。
  2. 用 time.time() 测微基准 —— 精度与单调性问题;用 perf_counter/timeit。
  3. 微优化成瘾(局部变量提前绑定、+= 换 join 之类)—— 节省的是纳秒级,污染的是整段代码;先大后小。
  4. 相信"总有一天要多进程" —— 先问:这段代码一年跑几次?一次性脚本的性能优化基本是伪需求。

10. 自测题

  1. 优化漏斗的前三步是什么?为什么顺序不能颠倒?
  2. timeit 与 cProfile 的分工?
  3. all_tags 从 list 换成 set 提速 30 倍的原理?
参考答案
  1. 算法 → 数据结构 → 减少工作;越靠前的收益是数量级级、越靠后的代码成本越高,先摘低垂果实。
  2. timeit 测单段代码的精确耗时(微基准);cProfile 找整个程序里函数级的耗时分布(热点定位)。
  3. list 的 in 是 O(n) 线性扫描,set 的 in 是 O(1) 哈希查找。

11. 与其他知识点的关系

  • kp-041 向量化:第 5 级漏斗的专业展开。
  • kp-029 GIL:第 6 级并行的约束条件。
  • kp-033 内存:性能与内存常是同一枚硬币。

12. 延伸阅读

  • 《High Performance Python》第 2 版
  • Python profiling 官方教程