當我們在說Python的效能優化時,我們在說什麼
所謂程式設計說的簡單一點就是為了達到某個目的,對位元進行傳輸和加工。而高效能程式設計就是以儘可能小的代價來傳輸和加工位元。在弄清楚如何在Python中實現高效能程式設計之前,弄清楚位元如何在真實的計算機系統中移動和加工很有必要。
計算機系統可以簡單劃分為三個部分:計算單元,儲存單元以及他們之間的連線單元。
計算單元
對於計算單元,我們最感興趣的是它在一個時鐘週期裡面可以執行多少次運算以及一秒鐘包含多少個時鐘週期。前者用IPC(Instructions Per Cycle)來衡量,後者用時鐘頻率來衡量。因為硬體的限制,IPC和時鐘頻率已經無法單方面繼續大幅提升了,為了加快運算速度,主要通過其他的方法來達到:超執行緒,亂序執行以及現在非常流行的多核架構。經常有人問,是不是核數越多程式跑的越快,答案是否定的,Amdahl’s law給出了完整的闡述,簡單說就是,程式中只能序列執行的比例越低且處理器越多,加速比越高,程式效率越高。也就是說,即使到了多核時代,提升程式的併發度仍然具有十分重要的意義。
儲存單元
對於儲存單元,讀寫速度和延遲是兩個主要效能指標。其中讀寫速度除了跟儲存介質有關,還跟資料讀取方式有直接關係,比如順序讀肯定隨機讀要快的多。從成本方面考慮,現代的儲存體系都是分層的,所有資料都是儲存在硬碟上,其中一部分會被載入到RAM中,更少一部分會被載入到L1/L2 cache。因此在優化程式的儲存模型時,需要考慮這個資料儲存在哪裡,如何組織以及它會被移動幾次。非同步I/O和預先式快取常用來加速訪問速度,因為都不需要等待資料獲取這個耗時的操作。
連線單元
連線單元有很多種變形,不過都可以統稱為匯流排。比如後端匯流排(連線L1/L2 cache和CPU)、前端匯流排(連線RAM和L1/L2 cache)、外部匯流排(連線CPU記憶體和硬碟網絡卡)。對於連線單元,有兩個主要效能指標:一次傳輸的資料量(匯流排頻寬)和一分鐘傳輸多少次(匯流排頻率)。其中大匯流排頻寬利於順序讀應用(一次大量),而高匯流排頻率則利於隨機讀應用(多次少量)。
在瞭解了計算機系統的三大基礎元件之後,可以從中瞭解到高效能程式設計的三個基本原則:
- 充分利用多核CPU,儘可能的並行處理
- 儘量把資料放在它被需要的地方
- 儘可能少的移動資料
而對於Python語言來講,Python的直譯器在底層計算資源的抽象上做了很多的工作。開發者完全不需要關心如何為陣列分配記憶體,如何組織記憶體以及它如何被傳送到CPU,從而讓開發者只需要關注業務功能的實現,所以Python上手快,開發效率高,但這些都是以Python執行效率低為代價的。
Python執行效率低的幾個原因:
- Python無法很容易利用CPU的向量化特性(單指令多資料流SIMD),numpy包可以支援。
- 因為Python是垃圾回收語言,在記憶體中必然會產生記憶體碎片,這對資料傳輸和儲存(特別是在L1/L2 Cache這個層面)都是不高效的
- Python的支援動態型別的非編譯語言,資料型別只有在執行時才能確定,直譯器無法提前對程式碼進行優化
- Python的GIL會限制程式利用現在無處不在的多核CPU