Python 代碼性能優化技巧


選擇了腳本語言就要忍受其速度,這句話在某種程度上說明了 python 作為腳本的一個不足之處,那就是執行效率和性能不夠理想,特別是在 performance 較差的機器上,因此有必要進行一定的代碼優化來提高程序的執行效率。

Python為什么性能差?

 

1、python是動態語言

一個變量所指向對象的類型在運行時才確定,編譯器做不了任何預測,也就無從優化。舉一個簡單的例子: r = a + b。 a和b相加,但a和b的類型在運行時才知道,對於加法操作,不同的類型有不同的處理,所以每次運行的時候都會去判斷a和b的類型,然后執行對應的操作。而在靜態語言如C++中,編譯的時候就確定了運行時的代碼。
另外一個例子是屬性查找,關於具體的查找順序在《python屬性查找》中有詳細介紹。簡而言之,訪問對象的某個屬性是一個非常復雜的過程,而且通過同一個變量訪問到的python對象還都可能不一樣(參見Lazy property的例子)。而在C語言中,訪問屬性用對象的地址加上屬性的偏移就可以了。

2、python是解釋執行

Python不支持JIT(just in time compiler)。雖然大名鼎鼎的google曾經嘗試Unladen Swallow 這個項目,但最終也折了。

3、python中一切都是對象

每個對象都需要維護引用計數,增加了額外的工作。

4、python GIL

GIL是Python最為詬病的一點,因為GIL,python中的多線程並不能真正的並發。如果是在IO bound的業務場景,這個問題並不大,但是在CPU BOUND的場景,這就很致命了。所以筆者在工作中使用python多線程的情況並不多,一般都是使用多進程(pre fork),或者在加上協程。即使在單線程,GIL也會帶來很大的性能影響,因為python每執行100個opcode(默認,可以通過sys.setcheckinterval()設置)就會嘗試線程的切換,具體的源代碼在ceval.c::PyEval_EvalFrameEx。

5、垃圾回收

這個可能是所有具有垃圾回收的編程語言的通病。python采用標記和分代的垃圾回收策略,每次垃圾回收的時候都會中斷正在執行的程序,造成所謂的頓卡。infoq上有一篇文章,提到禁用Python的GC機制后,Instagram性能提升了10%。

Python代碼優化常用技巧

  1.  減小代碼體積
  2. 提高代碼的運行效率

一、改進算法,選擇合適的數據結構(更好的選擇已經用藍色字標注出來了)

在算法的時間復雜度排序上依次是:

O(1) -> O(lg n) -> O(n lg n) -> O(n^2) -> O(n^3) -> O(n^k) -> O(k^n) -> O(n!)

當然選擇更合理的算法是最好的優化手段,但是在算法沒有辦法更加合理化的時候我們就要選擇更好的數據結構。

1、字典(dictionary)與列表(list)

Python 字典中使用了 hash table,因此查找操作的復雜度為 O(1),而 list 實際是個數組,在 list 中,查找需要遍歷整個 list,其復雜度為 O(n),因此對成員的查找訪問等操作字典要比 list 更快。

使用字典會比使用列表效率大概提高一半。

因此在需要多數據成員進行頻繁的查找或者訪問的時候,使用 dict 而不是 list 是一個較好的選擇。

2、集合(set)和列表(list)

set 的 union, intersection,difference 操作要比 list 的迭代要快。因此如果涉及到求 list 交集,並集或者差的問題可以轉換為 set 來操作。(附表一)

                                                        表 1. set 常見用法

語法 操作 說明
set(list1) | set(list2)  union  包含 list1 和 list2 所有數據的新集合
set(list1) & set(list2)  intersection  包含 list1 和 list2 中共同元素的新集合
set(list1) - set(list2)  difference  在 list1 中出現但不在 list2 中出現的元素的集合

 

3、對循環的優化

對循環的優化所遵循的原則是盡量減少循環過程中的計算量,有多重循環的盡量將內層的計算提到上一層。

4、充分利用Lazy if-evaluation 的特性

python 中條件表達式是 lazy evaluation 的,也就是說如果存在條件表達式 if x and y,在 x 為 false 的情況下 y 表達式的值將不再計算。
    所以在保證不改變運行結果的前提下,盡量減少IF里面的條件來提高程序的效率

5、字符串的優化

  1. 在字符串連接的使用盡量使用 join() 而不是 +;
  2. 當對字符串可以使用正則表達式或者內置函數來處理的時候,選擇內置函數。如 str.isalpha(),str.isdigit(),str.startswith(('x', 'yz')),str.endswith(('x', 'yz'));
  3. 對字符進行格式化比直接串聯讀取要快,因此要使用
1 out = "<html>%s%s%s%s</html>" % (head, prologue, query, tail)

而不是

1 out = "<html>" + head + prologue + query + tail + "</html>"

6、使用列表解析(list comprehension)和生成器表達式(generator expression)

列表解析要比在循環中重新構建一個新的 list 更為高效,因此我們可以利用這一特性來提高運行的效率。

1 for i in range (1000000): 
2   for w in list: 
3     total.append(w) 

使用列表解析:

1 for i in range (1000000): 
2   a = [w for w in list]

7、其他優化技巧

  1. 如果需要交換兩個變量的值使用 a,b=b,a 而不是借助中間變量 t=a;a=b;b=t;
  2. 在循環的時候使用 xrange 而不是 range;使用 xrange 可以節省大量的系統內存,因為 xrange() 在序列中每次調用只產生一個整數元素。而 range() 將直接返回完整的元素列表,用於循環時會有不必要的開銷;
  3. 使用局部變量,避免"global" 關鍵字。python 訪問局部變量會比全局變量要快得多;
  4. if done is not None 比語句 if done != None 更快;
  5. 在耗時較多的循環中,可以把函數的調用改為內聯的方式
  6. 使用級聯比較 "x < y < z" 而不是 "x < y and y < z";
  7. while 1 要比 while True 更快(當然后者的可讀性更好);
  8. build in 函數通常較快,add(a,b) 要優於 a+b。

二、定位程序性能瓶頸

使用 profile 進行性能分析

其中 Profiler 是 python 自帶的一組程序,能夠描述程序運行時候的性能,並提供各種統計幫助用戶定位程序的性能瓶頸。

profile 的使用非常簡單,只需要在使用之前進行 import 即可。具體實例如下:

1 import profile 
2 def profileTest(): 
3   Total =1; 
4    for i in range(10): 
5     Total=Total*(i+1) 
6        print Total 
7    return Total 
8 if __name__ == "__main__": 
9     profile.run("profileTest()")        

程序的運行結果如下:

其中輸出每列的具體解釋如下:

  • ncalls:表示函數調用的次數;
  • tottime:表示指定函數的總的運行時間,除掉函數中調用子函數的運行時間;
  • percall:(第一個 percall)等於 tottime/ncalls;
  • cumtime:表示該函數及其所有子函數的調用運行的時間,即函數開始調用到返回的時間;
  • percall:(第二個 percall)即函數運行一次的平均時間,等於 cumtime/ncalls;
  • filename:lineno(function):每個函數調用的具體信息;

三、Python性能優化工具

Python 性能優化除了改進算法,選用合適的數據結構之外,還有幾種關鍵的技術,比如將關鍵 python 代碼部分重寫成 C 擴展模塊,或者選用在性能上更為優化的解釋器等,這些在本文中統稱為優化工具。python 有很多自帶的優化工具,如 Pypy,Cython,Pyrex 等,這些優化工具各有千秋,本節選擇幾種進行介紹。

1、Pypy

PyPy 表示 "用 Python 實現的 Python",但實際上它是使用一個稱為 RPython 的 Python 子集實現的,能夠將 Python 代碼轉成 C, .NET, Java 等語言和平台的代碼。PyPy 集成了一種即時 (JIT) 編譯器。和許多編譯器,解釋器不同,它不關心 Python 代碼的詞法分析和語法樹。 因為它是用 Python 語言寫的,所以它直接利用 Python 語言的 Code Object.。 Code Object 是 Python 字節碼的表示,也就是說, PyPy 直接分析 Python 代碼所對應的字節碼 ,,這些字節碼即不是以字符形式也不是以某種二進制格式保存在文件中, 而在 Python 運行環境中。目前版本是 1.8. 支持不同的平台安裝,windows 上安裝 Pypy 需要先下載 ,然后解壓到相關的目錄,並將解壓后的路徑添加到環境變量 path 中即可。

接下來我們來測試一下使用同一個程序python解釋器和pypy解釋器的編譯時間為多少?到底有沒有提升速度?

1 import time
2 
3 t = time.time()
4 for i in range(10 ** 8):
5     continue
6 print(time.time() - t)

我們可以看到這是python解釋器的編譯時間:

 

 下面這是pypy解釋器的編譯時間:

從上面對比我們發現python解釋器的編譯時間為:5.84,

          而pypy解釋器的編譯時間為0.32,

 從編譯時間上我們可以看出速度提升了將近22倍!!

2、Cython

Cython 是用 python 實現的一種語言,可以用來寫 python 擴展,用它寫出來的庫都可以通過 import 來載入,性能上比 python 的快。cython 里可以載入 python 擴展 ( 比如 import math),也可以載入 c 的庫的頭文件 ( 比如 :cdef extern from "math.h"),另外也可以用它來寫 python 代碼。將關鍵部分重寫成 C 擴展模塊
Linux Cpython 的安裝可以參考文檔
Cython 代碼與 python 不同,必須先編譯,編譯一般需要經過兩個階段,將 pyx 文件編譯為 .c 文件,再將 .c 文件編譯為 .so 文件。編譯有多種方法:

  1. 通過命令行編譯
  2. 使用 distutils 編譯

下面來進行一個簡單的性能比較:

 1  from time import time 
 2  def test(int n): 
 3         cdef int a =0 
 4         cdef int i 
 5         for i in xrange(n): 
 6                 a+= i 
 7         return a 
 8 
 9  t = time() 
10  test(10000000) 
11  print "total run time:"
12  print time()-t

測試結果:

[GCC 4.0.2 20051125 (Red Hat 4.0.2-8)] on linux2 
 Type "help", "copyright", "credits" or "license" for more information. 
 >>> import pyximport; pyximport.install() 
 >>> import ctest 
 total run time: 
 0.00714015960693

使用python測試結果:

 

 通過清楚地對比可以發現使用 Cython 的速度提升了100多倍。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM