大神Bruce Eckel曾說:Life is short, you need Python! 隨後,Python創始人Guido就將印有「人生苦短,我用Python」的文化衫穿上身。
經過兩位大師的調侃式互動,加之自身較為強大的腳本語言優勢,Python近些年在大數據開發的常用高級語言中備受歡迎,大有超越Java的態勢;尤其在數據科學、深度學習領域,Python是當仁不二的選擇。
不過,當模型較複雜、運算量較大時,Python難免面臨運算速度慢的尷尬。那麼,選擇Python就必須要忍受它的性能短板嗎?
試試Cython吧!
Cython結合了Python的易用性和原生代碼的速度,可以說是便捷性與高性能的完美融合。
接下來,就請本期大數據專家——凌逍跟大家分享他使用Cython加速Python計算的實踐經驗。
提到Python的並行計算,不能不提極富爭議性的GIL(global interpreter lock),全局解釋器鎖。為了利用多核CPU的計算資源,Python並行只能使用進程,如multiprocessing庫等。如果用其他程式語言來寫,那麼開發者只需用把同步鎖就可以把線程之間的通信過程協調好,而在Python中,我們卻必須使用開銷較高的multiprocessing模塊。 multiprocessing的開銷之所以比較大,原因就在於: 主進程和子進程之間,必須進行序列化和反序列化操作。對於某些較為獨立,且數據量較小的任務來說,這套方案非常合適。所謂獨立,是指待運行的函數不需要與程序中的其他部分共享狀態。數據量較小是指主進程與子進程之間傳遞的數據比較小。如果待執行的運算不符合上述特徵,那麼multiprocessing所產生的開銷,可能無法通過並行化來提升程序速度。在那種情況下,也可以利用multiprocessing所提供的些高級機制,如共享內存等。不過,這些特性用起來非常複雜而且效率很低,而且會有內存佔用過多的問題。
下面通過一個簡單例子來說明:
字典nodeDict中存放的是編碼後的基站(key)與其他有切換關係的基站的列表(value)。我們想用與某個基站有切換關係的其他基站的經緯度來反向評估當前基站經緯度的異常。用最簡單的方法,計算切換基站列表的平均經緯度和當前基站的距離。距離越大則表示當前基站定位可能出現異常。對於這種簡單而量大計算,multiprocessing多進程加速的效率很低。
如下圖所示:
可見大部分時間都浪費在進程之間的通信上和序列反序列化上,多進程的效率比單進程的還低。對於這種簡單且量特別大的計算任務,多進程並行並不能加速任務。那麼在Python的生態圈內有沒有能使用線程來加速並行的神器呢?答案是有的,Cython從0.15版本開始就實現對原生並行編程的支持,使用OpenMP,讓Pythoner可以輕鬆的繞過GIL的限制。
Cython可以直接理解為一種非常類似Python的語言,實際上Cython是一種部分包含C語言,以及完全包含Pyhton語言的一個超集。Cython和Python的一個顯著區別就是,Cython的所有變量都可以明確聲明變量類型。相同的Python程序僅僅是添加了類型聲明就能在Cython中獲得近50%的速度提升。
但真正讓我們激動的是在Cython中可以無視GIL的存在而盡情使用線程加速,先來個簡單演示:
對變量x做累加,看起來我們得到了一個非常錯誤的答案,並且每次運行代碼時都會得到不同的答案,因為對x的寫操作不是安全的。這就是需要鎖的地方,在這種情況下,線程可以獲取x上的鎖,更新它,然後釋放鎖。這意味著一次只有一個線程在x上運行。將代碼更改如下:
這樣就能得到正確的結果。我們使用Cython來解決上面的問題,但不能再使用Python中的字典和列表,因為Python中的變量都自動帶了鎖(GIL)。還好Cython已經封裝了C++標準庫中的容器:deque,list,map,pair,queue,set,stack,vector。完全可以替代Python的dict, list, set等。
相互類型轉化規則如下:
下面的函數將上述的基站切換字典轉化成為了map類型,同理,計算距離的函數同樣需要改為nogil形式:
Cython中有三種定義函數的方式,def, cdef以及cpdef。其中def對應為Python函數:如在Python中定義函數一樣,以Python對象作為參數,返回Python對象。cdef為C函數,接受Python對象或C值作為參數,並且可以返回Python對象或C值,cdef函數不能直接在Python中調用。cpdef為混合函數,cpdef可以從任何地方調用,但是當從其他Cython代碼調用時使用更快的C調用。即使從Cython調用,cpdef也可以被子類或實例屬性上的Python方法覆蓋。
如果發生這種情況,大多數性能優勢會丟失。用Cython我們可以方便的向C代碼傳遞和返回結果,Cython會自動為我們做相應的類型轉化。
下面是我們定義的計算函數:
因為我們此次的數據量較小(約20萬),為了展現並行計算的加速,我們僅僅對中間的計算部分計時,並且沒有對結果進行收集。可以看到僅僅是單線程,Cython就將計算時間降至0.08秒左右,比Python版的程序速度提高了一百多倍。而且即使有更大的數據量,我們還可以開啟多線程加速,加速效率接近於線性加速。
Cython通過對Python代碼增加類型聲明和直接調用C/C++函數,使得從Python代碼中轉成等價的C/C++代碼的效率大大提高,而且它幾乎支持全部Python特性,可以說任何Python代碼都是有效的Cython代碼,這使得引入Cython技術的成本降到了最低。目前很多Python庫都使用Cython來提高效率,如pandas, scikit-learn, PyYAML等等。
玩轉Hadoop千節點Docker部署(上篇)
________________________________________
編輯:胡瑞
審核:凌逍、李光