# 练习 18:性能测量
> 原文:[Exercise 18: Measuring Performance](https://learncodethehardway.org/more-python-book/ex18.html)
> 译者:[飞龙](https://github.com/wizardforcel)
> 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/)
> 自豪地采用[谷歌翻译](https://translate.google.cn/)
在本练习中,你将学习使用多种工具来分析你创建的数据结构和算法的性能。为了使这个介绍专注并且简洁,我们将查看练习 16 中的`sorted.py`算法的性能,然后在视频中,我会分析我们迄今为止所做的所有数据结构的性能。
性能分析和调优是我最喜欢的计算机编程活动之一。在看电视的时候,我是那个手里拿着一团缠着的绳子的人,并且只打算把它解开,直到它很好并且有序。我喜欢探究复杂的奥秘,代码性能是最复杂的奥秘之一。有一些很好的并且实用的工具,用于分析代码的性能,使之比调试更好。
编码时不要试图实现性能改进,除非它们是显而易见的。我更喜欢使我的代码的初始版本保持极其简单和朴素,以便我可以确保它正常工作。然后,一旦它运行良好,但也许很慢,我启动我的分析工具,并开始寻找方法使其更快,而不降低稳定性。最后一部分是关键,因为许多程序员觉得如果能使代码更快,那么可以降低代码的稳定性和安全性。
## 工具
在本练习中,我们将介绍许多有用的 Python 工具,以及一些改进任何代码性能的一般策略。我们将使用的工具有:
+ [`timeit`](https://docs.python.org/3/library/timeit.html)
+ [`cProfile` 和 `profile`](https://docs.python.org/2/library/profile.html)
在继续之前,请确保安装任何需要安装的软件。然后获取`sorted.py`和`test_sorting.py`文件的副本,以便我们可以将这些工具应用到这些算法中。
### `timeit`
`timeit`模块不是非常有用。它所做的就是接受字符串形式的 Python 代码,并使用一些时间运行它。你不能传递函数引用,`.py`文件或除字符串之外的任何内容。我们可以在`test_sorting.py`的结尾,测试`test_bubble_sort`函数需要多长时间:
```py
if __name__ == '__main__':
import timeit
print(timeit.timeit("test_bubble_sort()", setup="from __main__ import test_bubble_sort"))
```
它也不会产生有用的测量或任何信息,为什么某些东西可能很慢。我们需要一种方式来衡量代码运行的时间长短,这样做太笨重了,无法使用。
### `cProfile`和`profile`
接下来的两个工具,对于测量代码的性能来说更为有用。我建议使用`cProfile`来分析代码的运行时间,并且当你在分析中需要更多的灵活性时,保存`profile`。为了对你的测试运行`cProfile`,请更改`test_sorting.py`文件的末尾,来简单地运行测试函数:
```py
if __name__ == '__main__':
test_bubble_sort()
test_merge_sort()
```
并将`max_numbers`更改为大约 800,或足够大的数字,以便你可以测量效果。一旦你完成了,然后在你的代码上运行`cProfile`:
```
$ python -m cProfile -s cumtime test_sorting.py | grep sorting.py
```
我使用了`| grep sorted.py`,只是将输出缩小到我关心的文件,但删除该部分命令可以查看完整的输出。我在相当快的电脑上获得的 800 个数字的结果是:
```
ncalls tottime percall cumtime percall filename:lineno(function)
1 0.000 0.000 0.145 0.145 test_sorting.py:1(<module>)
1 0.000 0.000 0.128 0.128 test_sorting.py:25(test_bubble_sort)
1 0.125 0.125 0.125 0.125 sorting.py:6(bubble_sort)
1 0.000 0.000 0.009 0.009 sorting.py:1(<module>)
1 0.000 0.000 0.008 0.008 test_sorting.py:33(test_merge_sort)
2 0.001 0.000 0.006 0.003 test_sorting.py:7(random_list)
1 0.000 0.000 0.005 0.005 sorting.py:37(merge_sort)
1599/1 0.001 0.000 0.005 0.005 sorting.py:47(merge_node)
7500/799 0.004 0.000 0.004 0.000 sorting.py:72(merge)
799 0.001 0.000 0.001 0.000 sorting.py:27(count)
2 0.000 0.000 0.000 0.000 test_sorting.py:14(is_sorted)
```
我在顶部添加了标题,以便你看到输出表示什么。每个标题的意思是:
> `ncalls`
> 该函数的调用次数
> `tottime`
> 总执行时间
> `percall`
> 函数每个调用的总时间
> `cumtime`
> 函数的累计时间
> `percall`
> 每个调用的累计时间
> `filename:lineno(function)`
> 名称、行号和涉及到的函数
那些标题名称也可以使用`-s`参数来获取。然后,我们可以对此输出进行快速分析:
`bubble_sort`被调用一次,但`merge_node`被调用了 1599 次,并且`merge `甚至调用了 7500 次。这是因为`merge_node`和`merge`是递归的,所以对一个有 800 个元素的随机列表排序时,他们会产生大量的调用。
即使`bubble_sort`不像`merge`或`merge_node`一样被频繁调用,它也是很慢的。这符合两种算法的性能预期。归并排序的最坏情况是`O(nlogn)`,但是对于冒泡排序,它是`O(n^2)`。如果你有 800 个元素,那么`800 * log(800)`约为 5347,而`800^2`是 640000!这些数字不一定会转化为这些算法运行的精确秒数,但它们确实会转化为相对比较。
`count`函数被调用 799 次,这最有可能是巨大的浪费。我们实现的`DoubleLinkedList`并不追踪元素的数量,而是必须在每一次你想知道数量的时候遍历这个列表。我们在这里的`count`函数中使用相同的方法,并且导致了整个列表中的 800 个元素的 799 次遍历。将`max_numbers`更改为 600 或 500 在这里查看规律。注意在我们的实现中,`count `是否运行了`n-1`次?这意味着我们遍历了几乎所有 800 个元素。
现在让我们查看,`dllist.py`如何影响其性能:
同样,我已经添加了标题,以便你可以看到发生了什么。在这种情况下,你可以看到,与`merge`,`merge_node`和`count`函数相比,`dllist.py`函数不会影响性能。这是很重要的,因为大多数程序员将运行优化`DoubleLinkedList`数据结构,但在`merge_sort`实现中可以获得更大的收益,并且完全可以避免使用`bubble_sort`。始终以最小的努力获得最大的改进。
## 性能分析
分析性能只是一件事情,找出什么较慢,然后试图确定为什么它较慢。它类似于调试,除了你最好不要改变代码的行为。完成后,代码的工作方式应该完全一样,仅仅是更快执行。有时修复性能也会发现错误,但是当你尝试加速时,最好不要尝试完全重新设计。一次只做一件事。
在开始分析性能之前,另一件重要的事情是,软件所需的一些指标。通常快即是好,但没有目标,你最终会提出一些完全不必要的解决方案。如果你的系统以 50 个请求/秒执行,并且你真的只需要 100 个请求/秒,那么没有必要使用 Haskell 完全重写它,来获得 200 的性能。这个过程完全关于,“节省最多的钱,并且付出最少的努力”,并且你需要某种测量作为目标。
你可以从运营人员那里获得大部分测量结果,并且应该有很好的图表,显示了 CPU 使用情况,请求/秒,帧速率,任何他们或客户认为重要的东西。然后,你可以与他们一起设计测试,证明一些缓慢的东西需要定位,以便你可以改进代码来达到所需的目标。你可以从系统中榨取更多的性能,从而节省资金。你可以尝试并得出结论,这只是一个需要更多 CPU 资源的难题。有了一个作为目标的指标,你会明白什么时候放弃,或已经做得足够了。
你可以用于分析的最简单过程是这样:
+ 在代码上运行性能分析器,就像我在这里使用测试所做的一样。你得到的信息越多越好。有关免费的其他工具,请参阅深入学习部分。向人们询问一些工具,它们用于分析系统的速度。
+ 识别最慢和最小的代码段。不要编写一个巨大的函数,并尝试分析它。很多时候这些函数很慢,因为它们使用了一大堆其他很慢的函数。首先找到最慢和最小的函数,你最有可能得到最大的收益,并付出最少的努力。
+ 审查这些缓慢的代码,和任何他们接触的代码,寻找代码缓慢的可能原因。循环内有循环吗?调用函数太频繁吗?在调查诸如缓存之类的复杂技术之前,寻找可以改变的简单事物。
+ 一旦你列出了所有最慢和最小的函数,以及简单的更改,使它们更快并寻找规律。你能在其它你看不到的地方做这件事吗?
+ 最后,如果没有简单更改你可以更改的小函数,可以寻求可能的较大改进。也许真的是完全重写的时候了吗?不要这样做,直到你至少尝试了简单的修复。
+ 列出你尝试的所有东西,以及你所完成的所有性能增益。如果你不这样做,那么你会不断地回到你已经处理过的函数上,并浪费精力。
在这个过程中,“最慢和最小”的概念是变化的。你修复了十几个 10 行的函数并使其更快,这意味着现在你可以查看最慢的 100 行的函数。一旦你让 100 行的函数运行得更快,你可以查看正在运行的更大的一组函数,并提出使其加速的策略。
最后,加速的最好办法是完全不做。如果你正在对相同条件进行多重检查,请找到避免多次检查的方法。如果你反复计算数据库中的同一列,请执行一次。如果你在密集的循环中调用函数,但数据不怎么改变,请缓存它或者事先计算出来。在许多情况下,你可以通过简单地事先计算一些东西,并一次性存储它们,来用空间换时间。
在下一个练习中,我们将会使用这个过程,来改进这些算法的性能。
## 挑战练习
此练习的挑战是,将我对`bubble_sort`和`merge_sort`所做的所有操作,都应用到目前为止所创建的所有数据结构和算法。我不期望你改进他们,但只是在开发测试来显示性能问题时,记下笔记并分析性能。抵制现在修改任何东西的诱惑,因为我们将在练习 19 中提高性能。
## 研究性学习
+ 到目前为止,对所有代码运行这些分析工具,并分析性能。
+ 将结果与算法和数据结构的理论结果进行比较。
## 破坏它
尝试编写使数据结构崩溃的病态测试。你可能需要为他们提供大量数据,但使用性能分析的信息来确保正确。
## 深入学习
+ 查看`line_profiler`,它是另一个性能测量工具。它的优点是,你只能衡量你关心的函数,但缺点是你必须更改源代码。
+ `pyprof2calltree`和`KCacheGrind`是更先进的工具,但老实说只能在 Linux 上工作。在视频中,我演示在 Linux 下使用它们。
- 笨办法学 Python · 续 中文版
- 引言
- 第一部分:预备知识
- 练习 0:起步
- 练习 1:流程
- 练习 2:创造力
- 练习 3:质量
- 第二部分:简单的黑魔法
- 练习 4:处理命令行参数
- 练习 5:cat
- 练习 6:find
- 练习 7:grep
- 练习 8:cut
- 练习 9:sed
- 练习 10:sort
- 练习 11:uniq
- 练习 12:复习
- 第三部分:数据结构
- 练习 13:单链表
- 练习 14:双链表
- 练习 15:栈和队列
- 练习 16:冒泡、快速和归并排序
- 练习 17:字典
- 练习 18:性能测量
- 练习 19:改善性能
- 练习 20:二叉搜索树
- 练习 21:二分搜索
- 练习 22:后缀数组
- 练习 23:三叉搜索树
- 练习 24:URL 快速路由
- 第四部分:进阶项目
- 练习 25:xargs
- 练习 26:hexdump
- 练习 27:tr
- 练习 28:sh
- 练习 29:diff和patch
- 第五部分:文本解析
- 练习 30:有限状态机
- 练习 31:正则表达式
- 练习 32:扫描器
- 练习 33:解析器
- 练习 34:分析器
- 练习 35:解释器
- 练习 36:简单的计算器
- 练习 37:小型 BASIC
- 第六部分:SQL 和对象关系映射
- 练习 38:SQL 简介
- 练习 39:SQL 创建
- 练习 40:SQL 读取
- 练习 41:SQL 更新
- 练习 42:SQL 删除
- 练习 43:SQL 管理
- 练习 44:使用 Python 的数据库 API
- 练习 45:创建 ORM
- 第七部分:大作业
- 练习 46:blog
- 练习 47:bc
- 练习 48:ed
- 练习 49:sed
- 练习 50:vi
- 练习 51:lessweb
- 练习 52:moreweb