翻譯自:http://scipy-lectures.github.com/advanced/optimizing/index.html html
做者:Gaël Varoquaux python
License:Creative Commons Attribution 3.0 United States License (CC-by) http://creativecommons.org/licenses/by/3.0/us git
過早的優化是罪惡的根源。 github
——Donald. Knuth 算法
這個章節涉及使Python代碼運行更快的策略。 數組
先決條件 緩存
目錄 dom
非測量,不優化 ide
在Ipython中,使用timeit
(http://docs.python.org/library/timeit.html)來計算基本運算時間。 函數
In [1]: import numpy as np In [2]: a = np.arange(1000) In [3]: %timeit a ** 2 100000 loops, best of 3: 3.55 us per loop In [4]: %timeit a ** 2.1 10000 loops, best of 3: 105 us per loop In [5]: %timeit a * a 100000 loops, best of 3: 3.5 us per loop
使用這個指引你的策略選擇。
注意:對於長時間運行的調用,使用%time
代替%timeit
;雖然精確度下降但運行更快。
當你有個很大的程序去分析時會有用,例如如下文件:
mport numpy as np from scipy import linalg from sklearn.decomposition import fastica # from mdp import fastica def test(): data = np.random.random((5000, 100)) u, s, v = linalg.svd(data) pca = np.dot(u[:10, :], data) results = fastica(pca.T, whiten=False) test()
在IPython中咱們能夠測量腳本運行時間:
In [6]: %run -t demo.py IPython CPU timings (estimated): User : 11.03 s. System : 0.43 s. Wall time: 13.12 s.
而後分析(profile)它:
1169 function calls in 10.765 seconds Ordered by: internal time ncalls tottime percall cumtime percall filename:lineno(function) 2 10.693 5.346 10.699 5.350 decomp_svd.py:14(svd) 144 0.040 0.000 0.040 0.000 {numpy.core.multiarray.dot} 1 0.015 0.015 0.015 0.015 {method 'random_sample' of 'mtrand.RandomState' objects} 20 0.005 0.000 0.007 0.000 function_base.py:526(asarray_chkfinite) 1 0.003 0.003 10.764 10.764 demo.py:1(<module>) 18 0.002 0.000 0.002 0.000 decomp.py:197(eigh) 17 0.001 0.000 0.001 0.000 fastica_.py:219(gprime) 40 0.001 0.000 0.001 0.000 {method 'any' of 'numpy.ndarray' objects} 17 0.001 0.000 0.001 0.000 fastica_.py:215(g) 1 0.001 0.001 0.008 0.008 fastica_.py:88(_ica_par) 1 0.001 0.001 10.764 10.764 {execfile} 52 0.000 0.000 0.001 0.000 twodim_base.py:220(diag) 18 0.000 0.000 0.003 0.000 fastica_.py:40(_sym_decorrelation) 18 0.000 0.000 0.000 0.000 {method 'mean' of 'numpy.ndarray' objects}
顯然svd
(在_decomp.py_中)是佔用咱們時間最多的東西,即瓶頸。咱們得找到一種方法來讓這一步運行更快,或者避免這一步(算法優化)。加速代碼剩下的部分並沒有益處。
分析器(profiler)很棒:它告訴咱們那個函數費時最多,但並非它在哪裏調用。
對此,咱們使用line_profiler:在原文件中,咱們用@profile
修飾一些咱們想要檢查的函數(不用導入它):
@profile def test(): data = np.random.random((5000, 100)) u, s, v = linalg.svd(data) pca = np.dot(u[:10, :], data) results = fastica(pca.T, whiten=False)
而後咱們使用kernprof.py程序,用-l和-v:
lyy@arch ~ % kernprof.py -l -v demo.py Wrote profile results to demo.py.lprof Timer unit: 1e-06 s File: demo.py Function: test at line 6 Total time: 10.5932 s Line # Hits Time Per Hit % Time Line Contents ============================================================== 6 @profile 7 def test(): 8 1 11070 11070.0 0.1 data = np.random.random((5000, 100)) 9 1 10530291 10530291.0 99.4 u, s, v = linalg.svd(data) 10 1 31026 31026.0 0.3 pca = np.dot(u[:10, :], data) 11 1 20766 20766.0 0.2 results = fastica(pca.T) kernprof.py -l -v demo.py 12.57s user 0.25s system 99% cpu 12.891 total
SVD佔用了大部分時間。咱們須要優化這行。
一旦咱們確認了瓶頸,咱們須要讓相應的代碼運行更快。
首先尋找算法優化:有沒有運算更少或更好的方式?
對於更高層次地看待問題,充分理解算法後的數學頗有幫助。然而,尋找簡單的改變並不尋常,像_移動計算1和在循環外分配內存2_,會帶來很大益處。
在以上每一個例子中,SVD——奇異值分解——是佔用時間最多的。確實,當輸入矩陣大小爲n時算法計算代價大約是n3。
然而,這些例子中,咱們都沒有使用SVD的輸出,可是僅僅使用它最開始返回的參數最初不多的幾行。若是咱們使用scipy的svd
實現,咱們能夠得到一個不完整的SVD版本。注意這個在scipy中的線性代數實現比numpy中的更加豐富,應該優先使用。
In [4]: %timeit np.linalg.svd(data) 1 loops, best of 3: 10.8 s per loop In [5]: from scipy import linalg In [6]: %timeit linalg.svd(data) 1 loops, best of 3: 10.4 s per loop In [7]: %timeit linalg.svd(data, full_matrices=False) 1 loops, best of 3: 278 ms per loop In [8]: %timeit np.linalg.svd(data, full_matrices=False) 1 loops, best of 3: 276 ms per loop
真正的不徹底SVD,例如僅僅計算前十個特徵向量,能夠用arpack3計算,能夠在scipy.sparse.linalg.eigsh
得到。
計算線性代數
對於肯定的算法,許多瓶頸將是線性代數計算。在本例中,使用正確的函數解決正確的問題是關鍵。例如,一個對稱矩陣的本徵值問題比一個普通矩陣更容易解決。一樣的,不少時候,你能夠避免反轉矩陣,而且使用代價更小(並更數值穩定)的運算。
瞭解你的線性代數計算。當有疑問時,探索scipy.linalg,而且用%timeit 來對你的數據嘗試不一樣的選擇。
一個完整的關於使用numpy的討論能夠在Advanced Numpy章節中找到,或者在van der Walt等人的文章The NumPy array: a structure for efficient numerical computation中。這裏咱們僅僅討論加速代碼運行速度常見的技巧。
向量化循環
找到技巧來避免使用numpy數組循環。對此,掩碼(masks)數組和索引(indices)數組會更有用。
廣播
使用廣播(broadcasting)來在結合它們以前對數組儘量少的運算。
在適當的位置運算
In [9]: a = np.zeros(1e7) In [11]: %timeit global a ; a *= 0 10 loops, best of 3: 29.1 ms per loop in [12]: %timeit global a ; a = 0*a 10 loops, best of 3: 54.3 ms per loop **注意:**咱們須要個`global a`讓timeit工做,由於它被賦給a,所以將它視做一個局部變量。
善待內存:使用視圖(views)而非拷貝(copies)
拷貝一個大數組就像對它們進行簡單的數值計算同樣耗費資源
In [18]: a = np.zeros(1e7) In [19]: %timeit a.copy() 10 loops, best of 3: 69 ms per loop In [20]: %timeit a + 1 10 loops, best of 3: 56.2 ms per loop
當心緩存影響(cache effects)
內存存取當是成組時是省資源的:以連續的方式存取一個大數組比隨機存取更快。這意味着除其它事項外更小的元素間距更快(參見CPU cache effects)4
In [21]: c = np.zeros((1e4, 1e4), order=’C’)
In [22]: %timeit c.sum(axis=0) 1 loops, best of 3: 3.62 s per loop In [23]: %timeit c.sum(axis=1) 10 loops, best of 3: 171 ms per loop In [24]: c.strides Out[24]: (80000, 8) In [25]: c = np.zeros((1e4, 1e4), order='F') In [26]: %timeit c.sum(axis=0) 1 loops, best of 3: 166 ms per loop In [27]: %timeit c.sum(axis=1) 1 loops, best of 3: 3.63 s per loop
這就是爲什麼Fortran順序或C順序可能對運算的影響很大:
in [28]: a = np.random.rand(20, 2**18) in [29]: b = np.random.rand(20, 2**18) in [30]: %timeit np.dot(b, a.T) 1 loops, best of 3: 278 ms per loop in [31]: c = np.ascontiguousarray(a.T) in [32]: %timeit np.dot(b, c) 1 loops, best of 3: 1.94 s per loop
注意拷貝數據來解決這個影響不值得:
In [34]: %timeit c = np.ascontiguousarray(a.T) 10 loops, best of 3: 45.4 ms per loop
使用numexpr來自動爲這種效應優化會頗有用。
使用編譯好的代碼
一旦你肯定全部高層次的優化都已經摸索過了,最後手段是將熱點,也就是耗費時間最多的代碼或函數,變成編譯好的代碼。對於編譯代碼,最優的選擇是使用Cython:它很輕鬆地讓你將已知的Python代碼轉換成編譯好的代碼,而且對numpy數組很好利用numpy支持產生有效率的代碼,例如經過循環展開(unrolling loops)。
Waring:對上述全部流程,分析(profile)而且計時(time)你的選擇。不要以理論爲依據來進行優化。