# 練習 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