- 内容提要
- 前言
- 作者简介
- 封面简介
- 第1章 理解高性能 Python
- 第2章 通过性能分析找到瓶颈
- 2.1 高效地分析性能
- 2.2 Julia 集合的介绍
- 2.3 计算完整的 Julia 集合
- 2.4 计时的简单方法——打印和修饰
- 2.5 用 UNIX 的 time 命令进行简单的计时
- 2.6 使用 cProfile 模块
- 2.7 用 runsnakerun 对 cProfile 的输出进行可视化
- 2.8 用 line_profiler 进行逐行分析
- 2.9 用 memory_profiler 诊断内存的用量
- 2.10 用 heapy 调查堆上的对象
- 2.11 用 dowser 实时画出变量的实例
- 2.12 用 dis 模块检查 CPython 字节码
- 2.13 在优化期间进行单元测试保持代码的正确性
- 2.14 确保性能分析成功的策略
- 2.15 小结
- 第3章 列表和元组
- 第4章 字典和集合
- 第5章 迭代器和生成器
- 第6章 矩阵和矢量计算
- 第7章 编译成 C
- 第8章 并发
- 第9章 multiprocessing 模块
- 第10章 集群和工作队列
- 第11章 使用更少的 RAM
- 第12章 现场教训
6.7 小结
回顾我们的优化历程,看起来我们使用了两种方法:减少CPU获得数据的时间和减少CPU需要干的工作。表6-1和表6-2在不同的矩阵大小上显示了我们在原始的纯Python实现之上进行的各种优化努力后的结果对比。
图6-4显示了这些优化手段之间的比较。我们可以看到两种优化手段的三个基带:底部的基带显示了我们纯Python实现在进行了降低内存分配次数之后的小小提升,中间的基带显示了我们使用numpy并进一步减少内存分配后发生了什么,最上面的基带则显示了减少进程总体工作量的结果。
表6-1 各种优化,各种矩阵大小,运行500次evolve函数的总时间
Method
256 × 256
512 × 512
1 024 × 1 024
2 048 × 2 048
4 096 × 4 096
Python
2.32s
9.49s
39.00s
155.02s
617.35s
Python + memory
2.56s
10.26s
40.87s
162.88s
650.26s
numpy
0.07s
0.28s
1.61s
11.28s
45.47s
numpy + memory
0.05s
0.22s
1.05s
6.95s
28.14s
numpy + memory+ laplacian
0.03s
0.12s
0.53s
2.68s
10.57s
numpy + memory+ laplacian + numexpr
0.04s
0.13s
0.50s
2.42s
9.54s
numpy + memory + scipy
0.05s
0.19s
1.22s
6.06s
30.31s
表6-2 各种优化,各种矩阵大小,运行500次evolve函数相比原生Python(例6-3)的速度提升倍数
Method
256 × 256
512 × 512
1 024 × 1 024
2 048 × 2 048
4 096 × 4 096
Python
0.00x
0.00x
0.00x
0.00x
0.00x
Python + memory
0.90x
0.93x
0.95x
0.95x
0.95x
numpy
32.33x
33.56x
24.25x
13.74x
13.58x
numpy + memory
42.63x
42.75x
37.13x
22.30x
21.94x
numpy + memory+ laplacian
77.98x
78.91x
73.90x
57.90x
58.43x
numpy + memory+laplacian +numexpr
65.01x
74.27x
78.27x
64.18x
64.75x
numpy + memory + scipy
42.43x
51.28x
32.09x
25.58x
20.37x
我们从中学到的一个重要的教训是你应该总是将代码需要的任何管理性工作放在初始化阶段进行。这可能包括内存分配,读取配置文件,预先计算程序所需的一些数据等。原因有两点。首先在初始化阶段一次性搞定可以让你减少这些工作运行的总次数,并让你知道你可以在将来不需要付出什么代价就使用这些资源。其次,你的程序不会因为要转而去做这些工作而打扰了流程,这可以让流水线更有效并让缓存始终含有相关数据。
我们同时还学到了数据的本地性以及将数据传给CPU这一简单操作的重要性有多高。CPU的缓存可以相当复杂,所以大多数时候我们都会让各种优化过的专用函数来处理它。但是,只要我们理解背后发生的故事并且用尽了各种可能的方法去优化内存的使用方法,那么结果会大不一样。比如,理解了缓存的工作方式,我们就能理解图6-4中的每一种优化手段在矩阵大到一定程度之后性能提升程度就不再发生变化的原因是我们的矩阵已经填满了L3缓存。建立内存分级制度本来是为了解决冯诺依曼瓶颈,但是当这种情况发生时,它反而成为了我们的制约。
另一个重要的收获是考虑使用各种外部库。Python本身是一门非常容易使用的高可读性语言,让你能够快速地编写和调试代码。但是使用外部库对于优化性能来说是必要的。这些外部库可以超级快,因为它们都是用各种低级语言写的——但是因为它们提供了Python接口,你依然可以迅速写出使用它们的代码。
最后,我们学到了运行性能测试并对结果做出假设的重要性。在测试之前先进行假设,我们就能让结果告诉我们优化是否真的成功了。这个改动是否能加速运行时间?是否减少了内存分配?是否降低了缓存失效的数量?现代计算机系统的复杂性让优化变成了一门艺术,而能对性能指标进行定量分析则是一个很大的帮助。
图6-4 本章各种优化方法带来的速度提升总结
优化的最后一点是必须花很大精力确保优化在各种计算机上都是通用的(你的假设以及测试结果可能跟运行程序的计算机架构以及模块编译方式等相关)。另外,在进行这些优化时还必须考虑到其他开发者,你的改动会多大程度上影响代码的可读性。比如,我们意识到例6-17中实现的解决方案具有一定的模糊性,所以特地确保代码具有完备的描述文档和测试来帮助我们以及团队中的其他成员。
下一章,我们会谈到如何创建你自己的外部模块来更高效地解决特定的问题。这让我们能够用快速原型的方式来写程序——首先用较慢的代码解决问题,然后找到导致慢的原因,最终让它们快起来。经常进行性能分析来找到并优化我们的慢速代码段,就能让自己的程序运行尽可能快的同时还省了自己的时间。
[1] 这段受分析代码来自例6-3,截取了部分以适应页面边界。别忘了kernprof.py需要函数被@profile修饰才能对其进行分析(见2.8节)
[2] 这段受分析代码来自例6-6,截取了部分以适应页面边界。
[3] 为此我们以-O0开关编译numpy。为了这个实验,我们用如下命令编译了numpy1.8.0: $ OPT='-O0' FOPT='-O0' BLAS=None LAPACK=None ATLAS=None python setup.py build。
[4] 这一点视实际使用的CPU而定。
[5] 这并不严格为真,因为两个numpy数组可以指向相同的内存区域但使用不同的步进信息来对同样的数 据做出不同的表达。这样两个numpy数组会具有不同的id。Numpy数组的id结构有很多微妙之处,在本书讨论范围之外。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论