第一原则:先测,再优化

"Premature optimization is the root of all evil." — Donald Knuth

不要凭感觉优化。先用 cProfile 找瓶颈,针对真正慢的地方下手。

timeit:测一段代码的速度

import timeit

# 比较两种写法
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)
print(t1, t2)

或在终端:

python -m timeit "[x*2 for x in range(1000)]"
# 1000 loops, best of 5: 230 usec per loop

适合对比小段代码

cProfile:扫描整个程序

python -m cProfile -s cumulative myapp.py

输出每个函数的调用次数、累计耗时,按 cumulative 排序——耗时最长的在最上面。

更直观:用 snakeviz 可视化:

pip install snakeviz
python -m cProfile -o profile.dat myapp.py
snakeviz profile.dat

浏览器里打开火焰图——一眼看出瓶颈。

数据结构选择决定 80% 性能

# 慢:list 的 in 是 O(n)
huge = list(range(1_000_000))
999_999 in huge        # 扫一遍,~10 ms

# 快:set 的 in 是 O(1)
huge_set = set(huge)
999_999 in huge_set    # 哈希查找,~50 ns

# 200,000 倍差距

对应规则:

操作 list set / dict
查找 in O(n) O(1)
增删 O(n) O(1)
索引访问 O(1) 不支持

经常 in 检查的容器一律用 set

字符串拼接

# 慢:每次拼接都创建新字符串
s = ""
for x in items:
    s += str(x)            # O(n²)!

# 快:一次性 join
s = "".join(str(x) for x in items)    # O(n)

局部变量比全局快

热循环里用局部变量:

import math

# 慢
for x in range(1_000_000):
    y = math.sqrt(x)

# 快
sqrt = math.sqrt        # 缓存到局部
for x in range(1_000_000):
    y = sqrt(x)

不大但有时候有用。

内置函数 / 内置类型 vs 手写

sum / map / filter / sorted / set 都是 C 实现的——比纯 Python 循环快几倍

# 慢
total = 0
for x in nums:
    total += x

# 快
total = sum(nums)

大量数值计算用 NumPy

纯 Python 处理百万数字会慢;NumPy 向量化操作快 100 倍:

import numpy as np

a = np.array(huge_list)
result = a ** 2 + 3 * a + 1   # 一行向量化

缓存 lru_cache

讲过——纯函数加 @lru_cache 立刻变快。

警惕这些"慢操作"

操作 替代
list.insert(0, x) collections.deque.appendleft
list.pop(0) deque.popleft
重复 str + str "".join
循环里查找 list 提前转 set
大数据循环 + 数学 NumPy

何时不要优化

如果代码已经够快、跑得不频繁,别花时间优化。可读性 > 性能。

记住:

  1. 先 profile——别瞎猜
  2. 改数据结构——立竿见影
  3. 用内置函数——免费的快
  4. 再想算法——O(n²) → O(n log n)
  5. 最后才想 C 扩展 / Cython——99% 用不到

下一篇讲内存管理