優化Python代碼

優化代碼

翻譯自: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

優化工做流

  1. 讓它工做:以簡單_清晰_的方式書寫代碼。
  2. 讓它可靠的動做:書寫自動化的測試實例,確認你的算法是正確的。若是你停止它,測試將捕捉到中斷。
  3. 優化代碼:經過剖析(profile)簡單的用例來發現瓶頸,而且加速這些瓶頸,找到更好的算法或實現。記住在剖析一個現實的實例和代碼的簡潔與執行速度之間權衡。對於有效率的工做,最好讓剖析運行約十秒。

剖析(profile)Python代碼

非測量,不優化 ide

  • 測量:剖析(profiling), 計時(timing)

Timeit

在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;雖然精確度下降但運行更快。

分析器(Profiler)

當你有個很大的程序去分析時會有用,例如如下文件:

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_中)是佔用咱們時間最多的東西,即瓶頸。咱們得找到一種方法來讓這一步運行更快,或者避免這一步(算法優化)。加速代碼剩下的部分並沒有益處。

Line-profiler

分析器(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的示例

在以上每一個例子中,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)你的選擇。不要以理論爲依據來進行優化。

  1. 存疑

  2. 我不懂

  3. Arpack——fortran數值計算庫

  4. python科學計算裏numpy部分講得很是清楚,第二種間距小的明顯快了不少。

相關文章
相關標籤/搜索