文章目录
-
- 性能优化的三个层次
- 层次一:诊断工具
- 层次二:算法优化
-
- 使用正确的数据结构
- [使用 `collections.deque` 替代 `list.pop(0)`](#使用
collections.deque替代list.pop(0)) - [使用 `functools.lru_cache` 缓存重复计算](#使用
functools.lru_cache缓存重复计算) - 使用生成器替代列表
- 循环优化:将不变计算提到循环外
- 层次三:底层加速
-
- [numba:JIT 编译提速](#numba:JIT 编译提速)
- [Cython:Python 的超集](#Cython:Python 的超集)
- [使用 `slots` 减少内存](#使用
__slots__减少内存)
- 性能优化决策树
- 性能优化的工程原则
-
- [1. 测量 → 优化 → 测量](#1. 测量 → 优化 → 测量)
- [2. 优化收益递减规律](#2. 优化收益递减规律)
- [3. 不要过早优化](#3. 不要过早优化)
- 综合实战:日志分析器性能优化
- 工具箱速查
- 优化优先级的黄金法则
- 系列结语
"Python 太慢了"------这句话对了一半。纯 Python 的 CPU 密集型计算确实慢,但性能优化从来不是从重写 C 开始,而是从找到瓶颈开始。
性能优化的三个层次
是
否
性能问题
层次一:诊断
cProfile + tracemalloc
找到瓶颈在哪
层次二:算法优化
数据结构 / 缓存 / 惰性求值
不用改语言,效果 10x+
优化后
是否满足需求?
完成 ✅
层次三:底层加速
Cython / Numba / C 扩展
热路径用编译代码替换
核心原则:没有测量就没有优化。盲目优化是浪费时间------80% 的执行时间通常集中在 20% 的代码中。先找到那 20%。
层次一:诊断工具
cProfile:找到最耗时的函数
python
import cProfile
import pstats
import io
import math
def is_prime(n: int) -> bool:
"""朴素素数判定------故意用低效实现做演示"""
if n < 2:
return False
for i in range(2, int(math.sqrt(n)) + 1):
if n % i == 0:
return False
return True
def find_primes(limit: int) -> list[int]:
"""找出范围内所有素数"""
primes = []
for n in range(limit):
if is_prime(n):
primes.append(n)
return primes
def main():
return find_primes(100_000)
# ===== 方式一:命令行 =====
# python -m cProfile -s cumtime script.py
# ===== 方式二:代码内 profile =====
if __name__ == "__main__":
profiler = cProfile.Profile()
profiler.enable()
result = main()
profiler.disable()
s = io.StringIO()
ps = pstats.Stats(profiler, stream=s).sort_stats("cumulative")
ps.print_stats(10) # 只显示前 10 个
print(s.getvalue())
输出示例:
ncalls tottime percall cumtime percall filename:lineno(function)
1 0.000 0.000 0.423 0.423 script.py:17(find_primes)
100000 0.413 0.000 0.413 0.000 script.py:8(is_prime)
1 0.000 0.000 0.423 0.423 script.py:24(main)
| 列 | 含义 |
|---|---|
ncalls |
调用次数(100000/1 表示 100000 次调用,1 次递归) |
tottime |
函数自身耗时(不含子调用) |
cumtime |
累计耗时(含所有子调用) |
关键发现 :is_prime 被调用了 100000 次,累计 0.423 秒------这就是瓶颈。优化目标明确。
line_profiler:逐行分析
cProfile 告诉你哪个函数慢,line_profiler 告诉你函数内部哪一行慢:
python
# 安装:pip install line_profiler
@profile # ← 不需要 import,line_profiler 自动处理
def slow_computation(data: list[int]) -> dict[str, float]:
result = {}
for item in data:
processed = item ** 2 # 行 7
result[str(item)] = math.log(processed + 1) # 行 8
return result
data = list(range(10000))
slow_computation(data)
# 命令行运行:
# kernprof -l -v script.py
输出:
Line # Hits Time Per Hit % Time Line Contents
6 10001 1234.0 0.12 5.2 for item in data:
7 10000 2380.0 0.24 10.0 processed = item ** 2
8 10000 20150.0 2.02 84.8 result[str(item)] = math.log(processed + 1)
发现 :第 8 行的 math.log 占了 84.8% 的时间。优化方向:预计算对数表、或用近似算法。
tracemalloc:内存泄漏与峰值定位
python
import tracemalloc
def memory_leak_demo():
"""演示 tracemalloc 如何定位内存泄漏"""
tracemalloc.start()
# 快照一:基准
snapshot1 = tracemalloc.take_snapshot()
# 模拟内存泄漏
leaked_data = []
for i in range(10000):
leaked_data.append([0] * 1000) # 每轮分配约 8KB
# 快照二:泄漏后
snapshot2 = tracemalloc.take_snapshot()
# 对比差异
stats = snapshot2.compare_to(snapshot1, "lineno")
print("Top 5 memory increases:")
for stat in stats[:5]:
print(f" {stat}")
print(f" +{stat.size_diff / 1024:.1f} KB, "
f"+{stat.count_diff} blocks")
print(f" {stat.traceback.format()[-1]}")
memory_leak_demo()
输出:
Top 5 memory increases:
script.py:15: size=78135 KiB (+78135 KiB), count=10000 (+10000)
+78135.0 KB, +10000 blocks
File "script.py", line 15
leaked_data.append([0] * 1000)
持续监控内存峰值
python
import tracemalloc
import time
def monitor_memory(target, args=(), duration: float = 10.0):
"""监控函数执行期间的内存峰值"""
tracemalloc.start()
start = time.perf_counter()
result = target(*args)
elapsed = time.perf_counter() - start
current, peak = tracemalloc.get_traced_memory()
tracemalloc.stop()
print(f"Function: {target.__name__}")
print(f" Duration: {elapsed:.2f}s")
print(f" Peak memory: {peak / 1024 / 1024:.1f} MiB")
print(f" Final memory: {current / 1024 / 1024:.1f} MiB")
return result
# 使用
def allocate_then_free():
data = [bytearray(1024 * 1024) for _ in range(50)] # 分配 50 MiB
time.sleep(1)
data.clear()
time.sleep(1)
monitor_memory(allocate_then_free)
层次二:算法优化
使用正确的数据结构
python
import time
import random
# === 场景:检查元素是否存在 ===
data = list(range(1_000_000))
check_values = [random.randint(0, 2_000_000) for _ in range(10_000)]
# ❌ 列表的 in 操作是 O(n)
start = time.perf_counter()
list_results = [v in data for v in check_values]
print(f"List search: {time.perf_counter() - start:.3f}s")
# ✅ 集合的 in 操作是 O(1)
data_set = set(data)
start = time.perf_counter()
set_results = [v in data_set for v in check_values]
print(f"Set search: {time.perf_counter() - start:.3f}s")
典型输出(差异可达 1000 倍):
List search: 1.234s
Set search: 0.001s
使用 collections.deque 替代 list.pop(0)
python
from collections import deque
import time
n = 100_000
# ❌ list.pop(0) 是 O(n)------后面的元素都要前移
lst = list(range(n))
start = time.perf_counter()
while lst:
lst.pop(0)
print(f"list.pop(0): {time.perf_counter() - start:.3f}s")
# ✅ deque.popleft() 是 O(1)
dq = deque(range(n))
start = time.perf_counter()
while dq:
dq.popleft()
print(f"deque.popleft: {time.perf_counter() - start:.3f}s")
使用 functools.lru_cache 缓存重复计算
python
from functools import lru_cache
import time
# ❌ 无缓存------每次递归都重新计算
def fib_naive(n: int) -> int:
if n < 2:
return n
return fib_naive(n - 1) + fib_naive(n - 2)
# ✅ LRU 缓存------相同参数只计算一次
@lru_cache(maxsize=None)
def fib_cached(n: int) -> int:
if n < 2:
return n
return fib_cached(n - 1) + fib_cached(n - 2)
# 对比
start = time.perf_counter()
result = fib_cached(35)
print(f"Cached fib(35): {time.perf_counter() - start:.6f}s")
# 对于无缓存版本,35 已经是灾难级别------这里只演示缓存的威力
使用生成器替代列表
python
import sys
# ❌ 一次加载所有数据到内存
def read_lines_list(filename: str) -> list[str]:
with open(filename) as f:
return f.readlines()
# ✅ 惰性逐行读取
def read_lines_generator(filename: str):
with open(filename) as f:
for line in f:
yield line.strip()
# 内存对比
lines_list = read_lines_list("large_file.txt")
lines_gen = read_lines_generator("large_file.txt")
print(f"List size: {sys.getsizeof(lines_list)} bytes") # 可能几百 MB
print(f"Generator size: {sys.getsizeof(lines_gen)} bytes") # 约 200 bytes
循环优化:将不变计算提到循环外
python
import math
# ❌ 每次迭代都计算 len(data) 和 math.sqrt
def slow_loop(data: list[float]) -> list[float]:
return [x * math.sqrt(len(data)) for x in data]
# ✅ 循环不变量提前计算
def fast_loop(data: list[float]) -> list[float]:
factor = math.sqrt(len(data))
return [x * factor for x in data]
层次三:底层加速
numba:JIT 编译提速
python
import numba
import numpy as np
import time
# ===== 纯 Python =====
def monte_carlo_pi_python(n: int) -> float:
"""蒙特卡洛法计算 π"""
inside = 0
for _ in range(n):
x = np.random.random()
y = np.random.random()
if x * x + y * y <= 1.0:
inside += 1
return 4.0 * inside / n
# ===== Numba JIT =====
@numba.jit(nopython=True)
def monte_carlo_pi_numba(n: int) -> float:
inside = 0
for _ in range(n):
x = np.random.random()
y = np.random.random()
if x * x + y * y <= 1.0:
inside += 1
return 4.0 * inside / n
# 对比(首次运行 Numba 有编译开销,所以先预热)
n = 10_000_000
# 纯 Python
start = time.perf_counter()
pi_py = monte_carlo_pi_python(n)
print(f"Pure Python: {time.perf_counter() - start:.2f}s, pi ≈ {pi_py}")
# Numba(预热后)
_ = monte_carlo_pi_numba(100)
start = time.perf_counter()
pi_nb = monte_carlo_pi_numba(n)
print(f"Numba JIT: {time.perf_counter() - start:.2f}s, pi ≈ {pi_nb}")
print(f"Speedup: {(time.perf_counter() - start):.0f}x") # 通常 10~100x
Cython:Python 的超集
python
# calc.pyx ------ Cython 源文件
# pip install cython
# 编译:python setup.py build_ext --inplace
def sum_of_squares(int n):
"""Cython 编译的平方和计算"""
cdef int i
cdef long long total = 0
for i in range(n):
total += i * i
return total
python
# setup.py
from setuptools import setup
from Cython.Build import cythonize
setup(
ext_modules=cythonize("calc.pyx"),
)
python
# 使用编译后的模块
from calc import sum_of_squares
result = sum_of_squares(10_000_000)
print(result)
Cython 的核心思想:在 Python 语法中添加 C 类型声明(cdef int i),编译为 C 扩展后获得接近 C 的性能。
使用 __slots__ 减少内存
在 Python 进阶 #20:__slots__ 进阶 中已详细讨论过------大量小对象场景下,__slots__ 可减少 50% 以上的内存占用:
python
class PointSlots:
__slots__ = ("x", "y")
def __init__(self, x, y):
self.x = x
self.y = y
class PointDict:
def __init__(self, x, y):
self.x = x
self.y = y
# 100 万个 Point,__slots__ 节省约 100 MiB
性能优化决策树
CPU 密集型
循环/计算
I/O 密集型
网络/文件/数据库
内存密集型
大量对象/大文件
是
否
数值计算
需要完整 C 控制
调用已有 C 库
性能不满足要求
运行 cProfile
定位瓶颈函数
瓶颈类型?
算法优化
数据结构、缓存、NumPy
并发优化
asyncio、线程池
内存优化
slots、生成器、mmap
算法优化后
满足需求?
完成 ✅
热路径是否
适合编译?
Numba JIT
简单装饰器,10x+
Cython
Python 超集,编译为 .so
ctypes / cffi
直接调用 .dll / .so
性能优化的工程原则
1. 测量 → 优化 → 测量
每次优化前先取基准,优化后验证效果:
python
import time
def benchmark(func, *args, runs: int = 5):
"""运行多次取最小值,排除系统抖动"""
times = []
for _ in range(runs):
start = time.perf_counter()
func(*args)
times.append(time.perf_counter() - start)
return min(times), sum(times) / len(times)
baseline_min, baseline_avg = benchmark(slow_function, data)
# ... 优化 slow_function ...
optimized_min, optimized_avg = benchmark(fast_function, data)
print(f"Baseline: {baseline_avg:.3f}s (min: {baseline_min:.3f}s)")
print(f"Optimized: {optimized_avg:.3f}s (min: {optimized_min:.3f}s)")
print(f"Speedup: {baseline_min / optimized_min:.1f}x")
2. 优化收益递减规律
python
# 优化投入 vs 回报
# 阶段一:算法优化(数据结构、缓存) → 10x ~ 1000x
# 阶段二:避免 Python 层面的浪费 → 2x ~ 10x
# (预计算、循环外提、生成器)
# 阶段三:编译加速(Numba、Cython) → 5x ~ 50x
# 阶段四:C 扩展重写核心逻辑 → 2x ~ 5x 在阶段三之上
# 阶段五:汇编级优化 → 微乎其微,几乎不值得
3. 不要过早优化
Donald Knuth 的名言值得铭记:"过早优化是万恶之源"------但不是"不要优化",而是在优化之前先有正确的实现和完整的测试。没有测试的优化是赌博:无法验证优化后的代码是否行为一致。
python
# 优化之前必须有的基础设施:
# 1. 完整的单元测试(验证行为一致性)
# 2. 性能基准测试脚本(量化效果)
# 3. 性能回归检测(CI 中监控性能退化)
# 在 CI 中监测性能退化:
# pytest-benchmark 可以自动比较本次运行与上一次的性能
综合实战:日志分析器性能优化
从慢到快的完整优化过程:
python
"""日志分析器------展示从 30 秒到 0.5 秒的优化过程"""
import re
import time
from collections import Counter
from typing import Iterator
# ===== 版本零:原始实现(基准) =====
def parse_log_original(filename: str) -> dict[str, int]:
"""逐行读取,正则匹配,字典统计"""
pattern = re.compile(r'\[(ERROR|WARNING|INFO)\]')
counts: dict[str, int] = {}
with open(filename) as f:
for line in f:
match = pattern.search(line)
if match:
level = match.group(1)
counts[level] = counts.get(level, 0) + 1
return counts
# ===== 版本一:优化数据结构(Counter 替代手写计数) =====
def parse_log_v1(filename: str) -> dict[str, int]:
"""Counter 替代手写计数------代码更简洁,性能微提"""
pattern = re.compile(r'\[(ERROR|WARNING|INFO)\]')
counter: Counter[str] = Counter()
with open(filename) as f:
for line in f:
match = pattern.search(line)
if match:
counter[match.group(1)] += 1
return dict(counter)
# ===== 版本二:预编译 + 快速路径 =====
def parse_log_v2(filename: str) -> dict[str, int]:
"""预编译正则 + 快速路径跳过无级别行"""
pattern = re.compile(r'\[(ERROR|WARNING|INFO)\]')
counter: Counter[str] = Counter()
with open(filename) as f:
for line in f:
# 快速路径:如果行不包含 '[',直接跳过
if '[' not in line:
continue
match = pattern.search(line)
if match:
counter[match.group(1)] += 1
return dict(counter)
# ===== 版本三:行级缓冲区 + 多行读取 =====
def parse_log_v3(filename: str, chunk_size: int = 65536) -> dict[str, int]:
"""大块读取 + 多行处理"""
pattern = re.compile(r'\[(ERROR|WARNING|INFO)\]')
counter: Counter[str] = Counter()
with open(filename) as f:
remainder = ""
while True:
chunk = f.read(chunk_size)
if not chunk:
break
lines = (remainder + chunk).split("\n")
remainder = lines.pop() # 最后一行可能不完整
for line in lines:
if "[" not in line:
continue
match = pattern.search(line)
if match:
counter[match.group(1)] += 1
return dict(counter)
# ===== 基准测试 =====
if __name__ == "__main__":
# 生成 100 万行日志文件
import random
levels = ["INFO", "INFO", "INFO", "INFO", "WARNING", "WARNING", "ERROR"]
with open("test.log", "w") as f:
for i in range(1_000_000):
level = random.choice(levels)
f.write(f"2024-01-01 12:00:{i % 60:02d} [{level}] Message {i}\n")
for name, func in [
("original", parse_log_original),
("v1 (Counter)", parse_log_v1),
("v2 (+skip)", parse_log_v2),
("v3 (+chunk)", parse_log_v3),
]:
start = time.perf_counter()
result = func("test.log")
elapsed = time.perf_counter() - start
print(f"{name:>15}: {elapsed:.3f}s → {result}")
工具箱速查
| 工具 | 用途 | 命令/用法 |
|---|---|---|
cProfile |
CPU 性能分析 | python -m cProfile -s cumtime script.py |
line_profiler |
逐行性能分析 | kernprof -l -v script.py |
tracemalloc |
内存追踪 | tracemalloc.start() + take_snapshot() |
memory_profiler |
逐行内存分析 | python -m memory_profiler script.py |
py-spy |
采样 profiler(无需修改代码) | py-spy top -- python script.py |
timeit |
微基准测试 | python -m timeit -s "setup" "stmt" |
numba |
JIT 编译 | @numba.jit(nopython=True) |
cython |
Python → C 编译 | .pyx 文件 + cythonize() |
ctypes |
调用 C 函数 | ctypes.CDLL("./lib.so") |
cffi |
调用 C 函数(更 Pythonic) | ffi.cdef("int func(int);") |
优化优先级的黄金法则
- 先测后优:没有 profile 数据的优化都是猜测
- 算法第一:换数据结构(list → set)比换语言更有效
- 减少工作:缓存重复计算、惰性求值、提前过滤------减少 CPU 做的无用功
- 用对库:NumPy 矩阵运算比 Python 循环快 100 倍------不是 Python 慢,是 Python 循环慢
- 编译是最后手段:Numba/Cython 只在算法优化到瓶颈时使用
- 优化后立即写测试:确保优化代码与原代码行为一致
系列结语
从 Python 基础的第一行 print("Hello World"),到本文的 C 扩展加速------Python 进阶系列的 30 篇文章涵盖了从"会用"到"用好"的全路径:闭包与装饰器、迭代器与生成器、上下文管理器、魔术方法与运算符重载、描述符与属性访问、类型注解与工程化、并发与异步进阶。
每一篇都在回答同一个问题:Python 为什么是这样设计的?------背后是面向对象的对象模型、是协程调度的事件循环、是类型系统在灵活与安全之间的权衡。
性能优化是工程的最后一公里。它让前面所有的知识都找到了落点------理解了数据模型才能写出对缓存友好的代码,理解了解释器机制才知道什么时候该跳出 Python 用 C 扩展。
如果这 30 篇文章对 Python 进阶之路有帮助,点赞收藏让更多人看到!关注专栏,更多技术系列持续更新中。