Python貓薦書系列之五:Python高效能程式設計
稍微關心程式語言的使用趨勢的人都知道,最近幾年,國內最火的兩種語言非 Python 與 Go 莫屬,於是,隔三差五就會有人問:這兩種語言誰更厲害/好找工作/高工資……
對於程式語言的爭論,就是猿界的生理週期,每個月都要鬧上一回。到了年末,各類榜單也是特別抓人眼球,鬧得更凶。
其實,它們各有對方所無法比擬的優勢以及用武之地,很多爭論都是沒有必要的。身為一個正在努力學習 Python 的(準)中年程式設計師,我覺得吧,先把一門語言精進了再說。沒有差勁的語言,只有差勁的程式設計師,等真的把語言學好了,必定是“山重水複疑無路,柳暗花明又一村”。
鋪墊已了,進入今天的正題,Python 貓薦書系列之五——
本書適合已入門 Python、還想要進階和提高的讀者閱讀。
所有計算機語言說到底都是在硬體層面的資料操作,所以高效能程式設計的一個終極目標可以說是“高效能硬體程式設計”。然而,Python 是一門高度抽象的計算機語言,它的一大優勢是開發團隊的高效,不可否認地存在這樣或那樣的設計缺陷,以及由於開發者的水平而造成的人為的效能缺陷。
本書的一大目的就是通過介紹各種模組和原理,來促成在快速開發 Python 的同時避免很多效能侷限,既減低開發及維護成本,又收穫系統的高效。
1、效能分析是基礎
首先的一個關鍵就是效能分析,藉此可以找到效能的瓶頸,使得效能調優做到事半功倍。
效能調優能夠讓你的程式碼能夠跑得“足夠快”以及“足夠瘦”。效能分析能夠讓你用最小的代價做出最實用的決定。
書中介紹了幾種效能分析的工具:
(1)基本技術如 IPython 的 %timeit 魔法函式、time.time()、以及一個計時修飾器,使用這些技術來了解語句和函式的行為。
(2)內建工具如 cProfile,瞭解程式碼中哪些函式耗時最長,並用 runsnake 進行視覺化。
(3)line_profiler 工具,對選定的函式進行逐行分析,其結果包含每行被呼叫的次數以及每行花費的時間百分比。
(4)memory_profiler 工具,以圖的形式展示RAM的使用情況隨時間的變化,解釋為什麼某個函式佔用了比預期更多的 RAM。
(5)Guppy 專案的 heapy 工具,檢視 Python 堆中物件的數量以及每個物件的大小,這對於消滅奇怪的記憶體洩漏特別有用。
(6)dowser 工具,通過Web瀏覽器介面審查一個持續執行的程序中的實時物件。
(7)dis 模組,檢視 CPython 的位元組碼,瞭解基於棧的 Python 虛擬機器如何執行。
(8)單元測試,在效能分析時要避免由優化手段帶來的破壞性後果。
作者強調了效能分析的重要性,同時也對如何確保效能分析的成功提了醒,例如,將測試程式碼與主體程式碼分離、避免硬體條件的干擾(如在BIOS上禁用了TurboBoost、禁用了作業系統改寫SpeedStep、只使用主電源等)、執行實驗時禁用後臺工具如備份和Dropbox、多次實驗、重啟並重跑實驗來二次驗證結果,等等。
效能分析對於高效能程式設計的作用,就好比複雜度分析對於演算法的作用,它本身不是高效能程式設計的一部分,但卻是最終有效的一種評判標準。
2、資料結構的影響
高效能程式設計最重要的事情是瞭解資料結構所能提供的效能保證。
高效能程式設計的很大一部分是瞭解你查詢資料的方式,並選擇一個能夠迅速響應這個查詢的資料結構。
書中主要分析了 4 種資料結構:列表和元組就類似於其它程式語言的陣列,主要用於儲存具有內在次序的資料;而字典和集合就類似其它程式語言的雜湊表/雜湊集,主要用於儲存無序的資料。
本書在介紹相關內容的時候很剋制,所介紹的都是些影響“速度更快、開銷更低”的內容,例如:內建的 Tim 排序演算法、列表的 resize 操作帶來的超額分配的開銷、元組的記憶體滯留(intern機制)帶來的資源優化、雜湊函式與嗅探函式的工作原理、雜湊碰撞帶來的麻煩與應對、Python 名稱空間的管理,等等。
雜湊碰撞的結果
理解了這些內容,就能更加了解在什麼情況下使用什麼資料結構,以及如何優化這些資料結構的效能。
另外,關於這 4 種資料結構,書中還得出了一些有趣的結論:對於一個擁有100 000 000個元素的大列表,實際分配的可能是112 500 007個元素;初始化一個列表比初始化一個元組慢5.1 倍;字典或集合預設的最小長度是8(也就是說,即使你只儲存3個值,Python仍然會分配 8 個元素)、對於有限大小的字典不存在一個最佳的雜湊函式。
3、矩陣和向量計算
向量計算是計算機工作原理不可或缺的部分,也是在晶片層次上對程式進行加速所必須瞭解的部分。
然而,原生 Python 並不支援向量操作,因為 Python 列表儲存的不是實際的資料,而是對實際資料的引用。在向量和矩陣操作時,這種儲存結構會造成極大的效能下降。比如,grid[5][2]
中的兩個數字其實是索引值,程式需要根據索引值進行兩次查詢,才能獲得實際的資料。
同時,因為資料被分片儲存,我們只能分別對每一片進行傳輸,而不是一次性傳輸整個塊,因此,記憶體傳輸的開銷也很大。
減少瓶頸最好的方法是讓程式碼知道如何分配我們的記憶體以及如何使用我們的資料進行計算。
Numpy 能夠將資料連續儲存在記憶體中並支援資料的向量操作,在資料處理方面,它是高效能程式設計的最佳解決方案之一。
Numpy 帶來效能提升的關鍵在於,它使用了高度優化且特殊構建的物件,取代了通用的列表結構來處理陣列,由此減少了記憶體碎片;此外,自動向量化的數學操作使得矩陣計算非常高效。
Numpy 在向量操作上的缺陷是一次只能處理一個操作。例如,當我們做 A * B + C 這樣的向量操作時,先要等待 A * B 操作完成,並儲存資料在一個臨時向量中,然後再將這個新的向量和 C 相加。
Numexpr 模組可以將矢量表達式編譯成非常高效的程式碼,可以將快取失效以及臨時變數的數量最小化。另外,它還能利用多核 CPU 以及 Intel 晶片專用的指令集來將速度最大化。
書中嘗試了多種優化方法的組合,通過詳細的分析,展示了高效能程式設計所能帶來的效能提升效果。
4、編譯器
書中提出一個觀點:讓你的程式碼執行更快的最簡單的辦法就是讓它做更少的工作。
編譯器把程式碼編譯成機器碼,是提高效能的關鍵組成部分。
不同的編譯器有什麼優勢呢,它們對於效能提升會帶來多少好處呢?書中主要介紹瞭如下編譯工具:
- Cython ——這是編譯成C最通用的工具,覆蓋了Numpy和普通的Python程式碼(需要一些C語言的知識)。
- Shed Skin —— 一個用於非Numpy程式碼的,自動把Python轉換成C的轉換器。
- Numba —— 一個專用於Numpy程式碼的新編譯器。
- Pythran —— 一個用於Numpy和非numpy程式碼的新編譯器。
- PyPy —— 一個用於非Numpy程式碼的,取代常規Python可執行程式的穩定的即時編譯器。
書中分析了這幾種編譯器的工作原理、優化範圍、以及適用場景等,是不錯的入門介紹。此外,作者還提到了其它的編譯工具,如Theano、Parakeet、PyViennaCL、ViennaCL、Nuitka 與 Pyston 等,它們各有取捨,在不同領域提供了支撐之力。
5、密集型任務
高效能程式設計的一個改進方向是提高密集型任務的處理效率,而這樣的任務無非兩大類:I/O 密集型與 CPU 密集型。
I/O 密集型任務主要是磁碟讀寫與網路通訊任務,佔用較多 I/O 時間,而對 CPU 要求較少;CPU 密集型任務恰恰相反,它們要消耗較多的 CPU 時間,進行大量的複雜的計算,例如計算圓周率與解析視訊等。
改善 I/O 密集型任務的技術是非同步程式設計 ,它使得程式在 I/O 阻塞時,併發執行其它任務,並通過“事件迴圈”機制來管理各項任務的執行時機,從而提升程式的執行效率。
書中介紹了三種非同步程式設計的庫:Gevent、Tornado 和 Asyncio,對三種模組的區別做了較多分析。
改善 CPU 密集型任務的主要方法是利用多核 CPU 進行多程序的運算。
Multiprocessing 模組使用基於程序和基於執行緒的並行處理,在佇列上共享任務,以及在程序間共享資料,是處理 CPU 密集型任務的重要技術。
書中沒有隱瞞它的侷限性:Amdahl 定律揭示的優化限度、適應於單機多核而多機則有其它選擇、全域性解釋鎖 GIL 的束縛、以及程序間通訊(同步資料和檢查共享資料)的開銷。針對程序間通訊問題,書中還分析了多種解決方案,例如 Less Naïve Pool、Manager、Redis、RawValue、MMap 等。
6、叢集與現場教訓
叢集是一種多伺服器執行相同任務的結構,也就是說,叢集中的各節點提供相同的服務,其優點是系統擴充套件容易、具備容災恢復能力。
叢集需要克服的挑戰有:機器間資訊同步的延遲、機器間配置與效能的差異、機器的損耗與維護、其它難以預料的問題。書中列舉了兩個慘痛的教訓:華爾街公司騎士資本由於軟體升級引入的錯誤,損失4.62億美元;Skype 公司 24 小時全球中斷的嚴重事故。
書中給我們重點介紹了三個叢集化解決方案:Parallel Python、IPython Parallel 和 NSQ。引申也介紹了一些普遍使用的方案,如 Celery、Gearman、PyRes、SQS。
關於現場教訓,它們不僅僅是一些事故或者故事而已,由成功的公司所總結出來的經驗更是來之不易的智慧。書中單獨用一章內容分享了六篇文章,這些文章出自幾個使用 Python 的公司/大型組織,像是Adaptive Lab、RadimRehurek、Smesh、PyPy 與 Lanyrd ,這些國外組織的一線實踐經驗,應該也能給國內的 Python 社群帶來一些啟示。
7、寫在最後
眾所周知,Python 應用前景大、簡單易學、方便開發與部署,然而與其它程式語言相比,它的效能幾乎總是落於下風。如何解決這個難題呢?本期薦書的書目就是一種迴應。
《Python高效能程式設計》全書從微觀到巨集觀對高效能程式設計的方方面面做了講解,主要包含以下主題:計算機內部結構的背景知識、列表和元組、字典和集合、迭代器和生成器、矩陣和向量計算、編譯器、併發、叢集和工作佇列等。這些內容為編寫更快的 Python 指明瞭答案。
本篇文章主要以梳理書中的內容要點為主,平均而兼顧地理清了全書脈絡(PS:介紹得太面面俱到了,但願不被指責為一篇流水賬的讀書筆記才好……)。我認為,鑑於書中談及的這些話題,它就足以成為我們薦書欄目的一員了。除去某些句段的糟糕翻譯、成書時間比較早(2014年)而造成的過時外,這本書總體質量不錯,可稱為是一份優秀的高效能程式設計的指引手冊。
關於薦書欄目,我最後多說幾句。本欄目原計劃兩週左右出一篇,但由於其它系列文章花費了我不少時間,而要寫好一篇薦書/書評也特別費勁,最後生生造成了現在兩月一更的尷尬局面……這篇文章是個錯誤的示範,我不該試圖全面通讀與概括其內容的。因此,我決定今後選一些易讀的書目,在寫作上也儘量走短小精悍風,希望能持續地將本欄目運作下去。若你有什麼建議(如書目推薦、書評推薦、寫作建議、甚至是投稿),我隨時歡迎,先行致謝啦。
往期薦書回顧:
第一期:《編寫高質量程式碼改善 Python 程式的 91 個建議》
第二期:《Python最佳實踐指南》
第三期:《黑客與畫家》
第四期:《Python原始碼剖析》
-----------------
本文原創並首發於微信公眾號【Python貓】,後臺回覆“愛學習”,免費獲得20+本精選電子書。