返回介绍

6.7 小结

发布于 2024-01-25 21:44:08 字数 6825 浏览 0 评论 0 收藏 0

回顾我们的优化历程,看起来我们使用了两种方法:减少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 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文