8.《Python高质量代码的91个建议》性能剖析与优化

1 建议 79:了解代码优化的基本原则

  • 优先保证代码是可工作的
  • 权衡优化的代价
  • 定义性能指标,集中力量解决首要问题
  • 不要忽略可读性

2 建议 80:借助性能优化工具

常见的性能优化工具有 PsycoPypycPython

Psyco是一个Python扩展模块,可以加快任何Python代码的执行速度,目前已停止维护~

Pypy作为CPython的替代(平均效率是CPython的4.2倍),支持大多数常用的Python标准库

Psyco项目地址 Pypy项目地址

3 建议 81:利用 cProfile 定位性能瓶颈

profile 是 Python 的标准库,可以统计程序里每一个函数的运行时间,并且提供了多样化的报表,而 cProfile 则是它的 C 实现版本,剖析过程本身需要消耗的资源更少。所以在 Python3 中,cProfile 成为默认的性能剖析模块。这些统计数据可通过 pstats 模块格式化为报告。

示例:

import cProfile
import re
cProfile.run('re.compile("foo|bar")')

结果如下:

197 function calls (192 primitive calls) in 0.002 seconds

Ordered by: standard name

ncalls  tottime  percall  cumtime  percall filename:lineno(function)
     1    0.000    0.000    0.001    0.001 <string>:1(<module>)
     1    0.000    0.000    0.001    0.001 re.py:212(compile)
     1    0.000    0.000    0.001    0.001 re.py:268(_compile)
     1    0.000    0.000    0.000    0.000 sre_compile.py:172(_compile_charset)
     1    0.000    0.000    0.000    0.000 sre_compile.py:201(_optimize_charset)
     4    0.000    0.000    0.000    0.000 sre_compile.py:25(_identityfunction)
   3/1    0.000    0.000    0.000    0.000 sre_compile.py:33(_compile)

具体可参考Python 分析器

4 建议 82:使用 memory_profilerobjgraph 剖析内存使用

Python 还提供了一些工具可以用来查看内存的使用情况以及追踪内存泄漏(如 memory_profilerobjgraphcProfilePySizerHeapy 等),或者可视化地显示对象之间的引用(如 objgraph),从而为发现内存问题提供更直接的证据。

memory_profiler是一个用于监控进程的内存消耗的python模块,能够逐行分析python程序的内存消耗。它是一个纯python模块,依赖于psutil模块。

memory_profiler使用示例:

# example.py
@profile
def my_func():
    a = [1] * (10 ** 6)
    b = [2] * (2 * 10 ** 7)
    del b
    return a

if __name__ == '__main__':
    my_func()
python -m memory_profiler example.py

# output
Line #    Mem usage    Increment  Occurences   Line Contents
============================================================
     3   38.816 MiB   38.816 MiB           1   @profile
     4                                         def my_func():
     5   46.492 MiB    7.676 MiB           1       a = [1] * (10 ** 6)
     6  199.117 MiB  152.625 MiB           1       b = [2] * (2 * 10 ** 7)
     7   46.629 MiB -152.488 MiB           1       del b
     8   46.629 MiB    0.000 MiB           1       return a

objgraph是一个用于可视化地探索Python对象的模块。

objgraph使用示例1(生成对象 x 的引用关系图):

import objgraph
x = ['a', '1', [2, 3]]
objgraph.show_refs([x])

objgraph使用示例1(显示对象数最多的三个类型):

objgraph.show_most_common_types(limit=3)
# output
function 28053 
dict 16973 
tuple 12292

memory_profiler项目地址 objgraph项目地址 PySizer项目地址(已停止维护) Heapy项目地址(已停止维护)

5 建议 83:努力降低算法复杂度

随着计算硬件资源发展,降低算法的时间复杂度更被重视

计算算法的时间复杂度时,既要注意平均时间复杂度,也要注意最差时间复杂度

O(1) < O(log * n) < O(n) < O(n log n) < O(n^2) < O(c^n) < O(n!) < O(n^n)

6 建议 84:掌握循环优化的基本技巧

  • 减少循环内部的计算
  • 显式循环改为隐式循环(比如用公式求解代替循环求解)
  • 在循环中尽量引用局部变量(搜索查询更快)
  • 嵌套循环时,尽量将内层循环的计算往上层移

7 建议 85:使用生成器提高效率

如果一个函数体中包含有 yield 语句,则称为生成器(generator)。 生成器是一种特殊的迭代器(iterator),也可以称为可迭代对象(iterable)。

生成器的优点:

  • 代码简洁优雅,生成迭代器的方式方便
  • 利用了惰性计算(Lazy evaluation)的特性,节省内存空间,提高效率
  • 使得协同程序(协程)更容易实现

8 建议 86:使用不同的数据结构优化性能

  • 插入、删除频繁的场景,可以考虑双端队列deque,它同时具备栈和队列的特性,在两端插入和删除时操作的复杂度为 O(1) ,目前deque对象被封装到了collections模块中,具体可参考deque 对象文档说明

  • 排序需求多的场景,可以考虑借助bisect模块维持常见数据结构的有序性,保持列表有序需要付出额外的成本,需要根据具体情况进行取舍,模块的进一步说明请参阅bisect文档--数组二分查找算法

  • 寻找最值的场景,可以考虑借助heapq模块,它借助二叉树结构实现了优先队列算法,并确保最小值永远在根节点,也就是列表的第一个元素,模块的进一步说明请参阅heapq文档--堆队列算法

  • 当容器存储的元素是同一类型时,可以考虑用 array 数据结构优化程序性能

9 建议 87:充分利用 set 的优势

Python 中集合是通过 Hash 算法实现的无序不重复的元素集。

计算交集、并集、补集的平均复杂度为 O(n),最差时间复杂度为 O(n^2),远好于list迭代计算

10 建议 88:使用 multiprocess 克服 GIL 的缺陷

1_study/ComputerScience/programming/进程、线程与协程#进程

11 建议 89:使用线程池提高效率

线程的生命周期分为 5 个状态:创建、就绪、运行、阻塞和终止。

线程的生命周期中真正占有 CPU 的只有运行、创建和销毁这 3 个状态。

线程池能避免线程的反复创建,从而节省线程创建和销毁的开销,带来更好的性能和系统稳定性

线程池技术适合处理的应用场景:

  • 突发性大量请求
  • 需要大量线程来完成任务、但任务实际处理时间较短

本小节提及的threadpool模块比较老了,目前仅推荐一些旧项目继续使用

现在更常用的是concurrent.futures模块中的ThreadPoolExecutor类,如需进一步了解可参阅ThreadPoolExecutor文档说明

12 建议 90:使用 C/C++ 模块扩展高性能

Python 具有良好的可扩展性,利用 Python 提供的 API,如宏、类型、函数等,可以让 Python 方便地进行 C/C++ 扩展,从而获得较优的执行性能。所有这些 API 却包含在 Python.h 的头文件中,在编写 C 代码的时候引入该头文件即可。

如需进一步了解可参阅Python/C API 参考说明

13 建议 91:使用 Cython 编写扩展模块

Python-API 让大家可以方便地使用 C/C++ 编写扩展模块,从而通过重写应用中的瓶颈代码获得性能提升。但是,这种方式仍然有几个问题:

  • 掌握 C/C++ 编程语言、工具链有巨大的学习成本

  • 即便是 C/C++ 熟手,重写代码也有非常多的工作,比如编写特定数据结构、算法的 C/C++ 版本,费时费力还容易出错

所以整个 Python 社区都在努力实现一个 ”编译器“,它可以把 Python 代码直接编译成等价的 C/C++ 代码,从而获得性能提升。这类工具有 Pyrex、Py2C 和 Cython 等。而从 Pyrex 发展而来的 Cython 是其中的集大成者。

Pyrex项目地址(已停止维护) Py2C项目地址 Cython项目地址

往年同期文章