Day 091 — 性能剖析 · 图解
性能剖析流程
┌─────────────────────────────────────────┐
│ 性能剖析完整流程 │
├─────────────────────────────────────────┤
│ │
│ 1️⃣ 确定目标 │
│ └→ 优化什么?为什么? │
│ └→ 目标性能指标是什么? │
│ │
│ 2️⃣ 基准测试 │
│ └→ 当前性能如何? │
│ └→ 用 timeit 建立基准 │
│ │
│ 3️⃣ 剖析代码 │
│ └→ cProfile → 找到热点函数 │
│ └→ line_profiler → 找到热点行 │
│ │
│ 4️⃣ 分析结果 │
│ └→ 哪些函数/行耗时最多? │
│ └→ 为什么慢? │
│ │
│ 5️⃣ 制定优化方案 │
│ └→ 算法优化?数据结构?缓存? │
│ │
│ 6️⃣ 实施优化 │
│ └→ 一次只改一个地方 │
│ │
│ 7️⃣ 验证效果 │
│ └→ 对比优化前后性能 │
│ └→ 确保没有引入 bug │
│ │
└─────────────────────────────────────────┘
cProfile 输出解读
6 function calls in 0.003 seconds
Ordered by: cumulative time
ncalls tottime percall cumtime percall filename:lineno(function)
1 0.001 0.001 0.003 0.003 main.py:10(process_data)
3 0.001 0.000 0.001 0.000 main.py:5(helper)
1 0.000 0.000 0.000 0.000 {method 'append' of 'list' objects}
1 0.000 0.000 0.000 0.000 {method 'items' of 'dict' objects}
关键列说明:
┌────────┬────────────────────────────────────┐
│ ncalls │ 函数被调用的次数 │
├────────┼────────────────────────────────────┤
│tottime │ 函数自身耗时(不含子函数) │
├────────┼────────────────────────────────────┤
│cumtime │ 累计耗时(含子函数) │
├────────┼────────────────────────────────────┤
│percall │ 每次调用平均耗时 │
└────────┴────────────────────────────────────┘
line_profiler 输出解读
Line # Hits Time Per Hit % Time Line Contents
==============================================================
3 def process_data(data):
4 1 0.1 0.1 1.2 result = []
5 1000 150.2 0.2 58.3 for item in data:
6 1000 80.5 0.1 31.2 transformed = item ** 2
7 1000 20.1 0.0 7.8 result.append(transformed)
8 1 8.5 8.5 3.3 return result
分析:
• 第5行: 循环占 58.3% 时间 → 主要瓶颈
• 第6行: 计算占 31.2% 时间 → 可优化
• 第7行: 追加占 7.8% 时间 → 合理
memory_profiler 输出解读
Line # Mem usage Increment Line Contents
================================================
3 38.5 MiB 38.5 MiB @profile
4 def analyze_memory():
5 38.5 MiB 0.0 MiB small_list = [1, 2, 3]
6 38.5 MiB 0.0 MiB small_dict = {'a': 1}
7 42.3 MiB 3.8 MiB large_list = [i for i in range(100000)]
8 46.1 MiB 3.8 MiB large_dict = {i: i**2 for i in range(100000)}
9 46.1 MiB 0.0 MiB result = sum(large_list)
10 42.3 MiB -3.8 MiB del large_list
分析:
• 第7行: 创建大列表增加 3.8 MiB
• 第8行: 创建大字典增加 3.8 MiB
• 第10行: 删除后内存释放
常见性能陷阱
┌─────────────────────────────────────────────┐
│ 常见性能陷阱 │
├──────────┬──────────────────────────────────┤
│ 循环中 │ ❌ for i in range(n): │
│ 重复查找 │ if i in my_list: # O(n)! │
│ │ ✅ my_set = set(my_list) │
│ │ if i in my_set: # O(1)! │
├──────────┼──────────────────────────────────┤
│ 字符串 │ ❌ result = "" │
│ 拼接 │ for s in strings: │
│ │ result += s # 每次新建! │
│ │ ✅ result = "".join(strings) │
├──────────┼──────────────────────────────────┤
│ 重复 │ ❌ for i in range(n): │
│ 计算 │ x = expensive_calc(i) │
│ │ ✅ cache = {i: expensive_calc(i) │
│ │ for i in range(n)} │
├──────────┼──────────────────────────────────┤
│ 在循环中 │ ❌ for i in range(n): │
│ 创建对象 │ regex = re.compile(r'\d+') │
│ │ ✅ regex = re.compile(r'\d+') │
│ │ for i in range(n): │
└──────────┴──────────────────────────────────┘
性能优化策略
优化策略优先级:
┌─────────────────────────────────────────┐
│ 1. 算法优化 (最重要) │
│ O(n²) → O(n log n) → O(n) │
├─────────────────────────────────────────┤
│ 2. 数据结构选择 │
│ list → set/dict (查找 O(n) → O(1)) │
├─────────────────────────────────────────┤
│ 3. 减少遍历次数 │
│ 多次遍历 → 单次遍历 │
├─────────────────────────────────────────┤
│ 4. 缓存结果 │
│ 重复计算 → 缓存 │
├─────────────────────────────────────────┤
│ 5. 延迟加载 │
│ 提前加载 → 按需加载 │
├─────────────────────────────────────────┤
│ 6. 批量处理 │
│ 逐个处理 → 批量处理 │
└─────────────────────────────────────────┘
timeit 使用方法
timeit 基本用法:
┌─────────────────────────────────────────┐
│ # 代码字符串计时 │
│ timeit.timeit('sum(range(1000))', │
│ number=10000) │
├─────────────────────────────────────────┤
│ # 函数计时 │
│ timeit.timeit(my_func, │
│ number=10000) │
├─────────────────────────────────────────┤
│ # 对比两种方法 │
│ t1 = timeit.timeit( │
│ '[x**2 for x in range(1000)]', │
│ number=10000) │
│ t2 = timeit.timeit( │
│ 'list(map(lambda x: x**2, │
│ range(1000)))', │
│ number=10000) │
└─────────────────────────────────────────┘