
简介 1. 阿姆达尔定律 2. 古斯塔夫森定律 3. Python 中的编程性能分析工具
阿姆达尔定律由G.M. Amdahl在1967年提出,用于描述并行处理系统的加速比。其核心思想是:系统的加速比受限于程序中必须串行执行的部分。公式如下:
其中p 指的是可并行化的代码的时间占比,N 指的是处理器的数量。阿姆达尔定律可以非常粗略的估计并行化的程序的潜在加速比。阿姆达尔定律考虑的是固定规模问题下速度的上限。
古斯塔夫森定律则从可扩展问题(scaled problem)的角度出发,认为随着处理器数量增加,可以处理更大的问题,从而获得更高的加速比。公式如下:
其中a 是必须串行部分的代码执行时间,b 是可以并行的部分代码执行时间,n 是处理器数量。
当 F 表示必须串行部分代码的时间占比:
那么会有:
从公式中可以看出,F (串行化程度)足够小,也即并行化足够高,那么加速比和cpu个数成正比。
下面这套流程是我在沙盒里实跑验证过的(Python 3.10 + line_profiler 5.0.2 + memory_profiler 0.61),所有输出都是真实结果,不是示例文本。
一、先选对工具
工具 | 粒度 | 依赖 | 典型开销 | 什么时候用 |
|---|---|---|---|---|
cProfile | 函数级 | 内置 | 低 | 第一步:全局扫描,找出"哪个函数慢" |
line_profiler | 逐行时间 | pip 装 | 高 | (10–30×) 第二步:热点函数里"哪一行慢" |
memory_profiler | 逐行内存 | pip 装 | 很高 | (10–100×) 内存涨/泄漏时,定位"哪一行吃内存" |
使用步骤建议:cProfile 定位函数 → line_profiler 定位行 → memory_profiler 查内存。一上来就逐行 profile 全程序,程序会慢到不可用的程度。
命令行(最常用)
python3 -m cProfile -s cumtime demo_slow.py # 按累计耗时排序
python3 -m cProfile -s tottime demo_slow.py # 按自身耗时排序(排除子调用)
python3 -m cProfile -o demo.prof demo_slow.py # 落盘,之后离线分析实测输出:
215034 function calls (63925 primitive calls) in 0.361 seconds
Ordered by: cumulative time
ncalls tottime percall cumtime percall filename:lineno(function)
1 0.002 0.002 0.361 0.361 demo_slow.py:1(<module>)
1 0.002 0.002 0.287 0.287 demo_slow.py:33(main)
1 0.007 0.007 0.210 0.210 demo_slow.py:19(process)
1 0.201 0.201 0.201 0.201 {built-in method time.sleep}
150049/1 0.027 0.000 0.027 0.027 demo_slow.py:11(fib)字段含义:
• ncalls:调用次数。150049/1 是"总次数/非递归次数",斜杠说明这是递归函数。
• tottime:函数自身耗时,不含它调用的子函数 —— 找"自己就很慢"的函数用它排。
• cumtime:累计耗时,含所有子调用 —— 找"这条调用链拖慢了整体"用它排。
• percall:tottime/ncalls。
一眼看出:process 的 cumtime 0.210s 里,0.201s 全在 time.sleep —— sleep/IO 等待也算进 cumtime,这不是 CPU 瓶颈。这就是 cumtime 最大的陷阱。
cProfile 的盲区:看不到行;C 扩展(numpy/pandas 内部)不展开;多线程默认只测主线程;开销会让小函数失真。
下载方式
pip install line_profiler被 profiling 的函数加 @profile 装饰器,不需要 import 任何东西 —— kernprof 会把 profile 注入 builtins。
@profile
def process(rows):
raw = ""
for i in range(rows):
raw += str(i) + "," # 字符串不可变,O(n²)
time.sleep(0.2)kernprof -l -v demo_slow.py # -l 逐行,-v 跑完立刻打印
kernprof -l demo_slow.py # 只生成 demo_slow.py.lprof
python3 -m line_profiler demo_slow.py.lprof # 之后随时离线查看实测输出:
Timer unit: 1e-06 s
Total time: 0.218204 s
Function: process at line 13
Line # Hits Time Per Hit % Time Line Contents
==============================================================
16 1 0.6 0.6 0.0 raw = ""
18 20001 2235.3 0.1 1.0 for i in range(rows):
19 20000 5429.0 0.3 2.5 raw += str(i) + ","
20 1 631.6 631.6 0.3 parts = raw.split(",")
23 1 201208.3 201208.3 92.2 time.sleep(0.2)• Hits:该行执行次数(循环体行会很大)
• Time:该行总耗时;Per Hit:单次耗时 → 判断"是跑得太多还是单次太慢"
• % Time:占该函数比例
⚠️ 每次都先看 Timer unit:上面是 1e-06 s,但用显式 API 打印时我实测得到的是 Timer unit: 1e-09 s(纳秒),数字会差 1000 倍。别跨报告直接比绝对值,只看百分比。
from line_profiler import LineProfiler
import demo_slow
lp = LineProfiler()
lp.add_function(demo_slow.process) # 只盯这一个函数,开销最小
lp(demo_slow.main)() # 或用 with lp: 包裹代码块
lp.print_stats()直接 python3 demo_slow.py 会因 @profile 未定义报 NameError。加个兜底:
try: # line_profiler >= 4.1:不用 kernprof 运行时,profile 是透明的空装饰器
from line_profiler import profile
except ImportError:
def profile(func):
return func下载步骤:
pip install memory_profiler psutil # psutil 让采样快很多逐行报告
from memory_profiler import profile # 注意:这里必须显式 import
@profile
def main():
rows = build_rows(120000)
text = [str(i) for i in range(200000)]
del rows # 释放后能看到内存下降python3 demo_mem.py # 有装饰器时直接跑就会打印
python3 -m memory_profiler demo_mem.py # 或显式指定实测输出:
Line # Mem usage Increment Occurrences Line Contents
=============================================================
16 65.1 MiB 31.0 MiB 1 rows = build_rows(120000)
17 78.8 MiB 13.8 MiB 200003 text = [str(i) for i in range(200000)]
18 51.4 MiB -27.4 MiB 1 del rows
19 51.4 MiB 0.0 MiB 1 keep = text[: len(text) // 2]• Mem usage:执行完该行后进程的 RSS
• Increment:相对上一行的净增量(不是累计分配量!所以 del 行是负数,这正是找"内存没释放"的关键)
• Occurrences:该行执行次数
时间曲线(看泄漏最直观)
mprof run -o mem.dat demo_mem.py # 不需要任何装饰器,每 0.1s 采样一次 RSS
mprof plot -o mem_curve.png mem.dat # 出图(需 matplotlib)• py-spy:py-spy top --pid <pid> / py-spy record,采样式,能直接分析正在跑的生产进程,开销 <1%,无需改代码
• scalene:同时给出 CPU / GPU / 内存,且能区分 Python 代码与 C 扩展耗时,精度远高于 memory_profiler
• pyinstrument:调用树式输出,比 cProfile 的扁平列表易读
• tracemalloc:内置,查"哪一行分配了最多 Python 对象",内存问题的首选交叉验证手段
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。