Python | 為什麼優化代碼?
通過優化代碼來降低運行時間是一件十分有意義的工作。然而,大部分情況下,優化是編寫軟體的最後一個步驟,並且通常是在不得不優化的情況下進行的。如果你的代碼可以在能夠接受的時間範圍內運行完成,那麼優化並不是必需的。
鑒於此,為什麼我們還要優化代碼呢?作為數據科學家,我們現在面對的是越來越大規模的數據集,在每個元素上進行的操作可能要重複數十億次才能得到最後的結果。如果代碼質量很低,可能需要花費數天才能完成運行。此外,大部分科學計算和數值軟體是計算密集型而不是網路密集型的任務。鑒於數據計算中使用的模型也越來越複雜,每一點的性能下降都會產生嚴重的後果。那麼優化代碼還有什麼其他的原因嗎?原因如下。
- 時間就是金錢:如果你是在雲端運行程序,那麼時間就是金錢。額外的每一分鐘處理都需要付費,當程序在數千台機器上的時候更是如此。而且要注意的是,Amazon EC2是按小時計費,而不是像Google Compute Engine那樣按分鐘計費。這就意味著在EC2上運行61分鐘要比運行59分鐘多出一倍的費用。
- 迭代次數:循環次數,或者是在指定截止日前迭代的次數是另一個十分重要的問題。如果你的分析程序需要一個星期才能運行出結果,那麼你就無法像研究只需運行24小時的程序那樣,不斷解析每次迭代過程的結果,中斷運行,更新結果。相信我,你不可能在第一次甚至第三次都得到完美的分析。多輪迭代十分重要。
- 生產部署要求:生產環境對代碼運行的時間有更高的要求。你的代碼能否實時給出結果?你的計算能否在用戶等厭煩而離開前給出結果?或者你的代碼是否需要批量地通宵運行?是否有一個運行完成的時間窗口?將你的代碼從原型轉化至批量上線運行的過程中,執行時間是一個十分重要的問題。
- 能耗節約:完成同樣的任務,若可以使用調用更少指令、消耗更少的內存和更少的CPU時間的代碼,會節省很多能源。如果你需要考慮移動設備的能源消耗或者伺服器的熱力性能,請優化你的代碼。
- 為從大數據到大計算的轉換做準備:現在這個時代是大數據的時代(對於一般的數據科學家來說,這和他們息息相關),一旦人們厭倦了統計點擊次數和簡單的大規模分析,人們會開始進行更多更有意思的計算。很快我們將從大數據時代進入大計算時代,CPU時間將超過存儲和帶寬成為最寶貴的資源。到那時,代碼優化將成為極其重要的一個環節,高性能計算(HPC)領域的工作將逐漸成為主流。
- 樂趣以及個人提高:從代碼中榨取出更多的性能是個十分有意思的謎題。這需要你極其深入地了解所使用的編程語言(了解語言作者對語言核心部分的設計),並且在極端情況下需要了解底層硬體的能力和限制。現如今,高級語言有各種內建的庫和框架來幫助你處理幾乎所有的事情,很少有人擁有對語言這種深度的理解。
了解優化的步驟
本小節將簡要介紹軟體優化的步驟。
處理流程
下面的內容介紹了我們將要使用的代碼優化步驟。
1.建立現有代碼的基線性能(執行時間、內存消耗、峰值I/O帶寬等)。
2.確定性能目標以及系統的上限。以終為始是高效能人士的七個習慣之一,優化也是同樣的道理。代碼需要執行多快?完成任務可接受的最長時間是多少?軟體最大可以佔用多少內存?計算結果需要多準確?結果需要怎樣的保真度?
3.建立測試和度量環境,可以幫助你迅速測量相關的性能指標。如果能夠更容易地度量每一行代碼的性能,那麼就可以更快地優化代碼。如果度量的過程十分痛苦並且總要去記一些你會忘記的命令,優化的過程就會變得痛苦而緩慢。
4.記錄當前代碼的所有參數和狀態快照。
5.利用剖析器尋找代碼瓶頸。
6.從最主要的瓶頸入手來解決性能問題。鑒於代碼的複雜度,每輪測試只解決一個問題是一種安全的方法。
7.利用剖析器分析修改後的代碼,檢查結果是否有變化。
8.跳回第4步,儘可能多地重複進行。
工作原理
和粉刷房屋一樣,你需要做好充足的準備才能取得成功。在刷子觸碰到牆壁前,你需要挪走牆邊的所有傢具並給它們蓋上布。地板通常被一塊干布保護起來。畫、架子和其他掛載在牆上的一些東西都必須取下來,釘子也必須拔掉。還需要用濕布擦拭牆來除掉灰塵,並處理那些需要修補的地方。接下來,你需要封住那些不需要被粉刷的部分,如門邊、窗邊、電源插座。所有這些都做好後,真正粉刷牆只需要花很少的時間。優化也和這個過程類似,你必須創建好一個利於優化進行的環境。因此上面的第3步「建立測試和度量環境」是十分重要的一步。
在優化代碼的過程中,你會不斷停下來測試大大小小的代碼變更。一些工作正常,一些會使代碼無法工作。一些會提高性能,而另一些不會。當你不斷停下來進行測試時,能夠恢復四五次之前性能最好的可工作代碼變得異常重要。同樣重要的是,要記住每次變更所帶來的性能影響。當你測試了一組代碼變化後,你需要知道保留哪些變化。Git、Mercurial 和其他一些代碼版本管理工具都是你的好幫手。
在第6步中,這種處理瓶頸的順序通常是從簡至難。如果主要的瓶頸需要重寫大量的代碼才能解決,那麼可以先去解決那些只需要一行代碼或者很少的變更即可解決的次要性能瓶頸。
更多內容
每一行可能帶來性能提升的代碼同樣可能引入新的問題。因此在優化代碼時,不要去做和優化性能無關的事情。最糟糕的優化可能會帶來難以檢測的問題,甚至代碼行為的不一致。同樣需要注意的是,優化代碼的過程可能會使你自己和需要接手代碼的人越來越難讀懂代碼。
識別代碼中常見性能瓶頸
在優化數據科學項目的過程中,查看整個分析過程、了解每個階段所需要消耗的時間以及它們之間的關係是十分重要的。
讓我們把問題簡化成減少軟體的執行時間,且只考慮使用一種特定的語言來實現這個軟體。在這裡,我們不考慮處理大數據集,也就是說,將數據規模從生產資料庫規模降到簡單分析用的數據。
從更抽象的層次來說,執行時間只和代碼本身以及硬體條件相關。如果想要降低運行時間,要麼修改代碼,要麼升級硬體,或者兩者同時進行。
對於優化來說,我們必須時刻記住自己的目的是什麼:必須達到何種程度的優化或者軟體需要運行多快。
將運行時間降為原來的二分之一和降低一個數量級是兩種完全不同的優化方式,通常後者需要代碼發生根本性的變化。
處理流程
剖析器(一種可以提供其他軟體運行時信息的軟體)可以幫助你找到運行最慢的代碼行或者代碼塊。通過觀察,你可以發現存在某種特定的代碼類型、代碼模式或者問題域,可以提高代碼的速度。
1.留心任何的循環,尤其是嵌套循環。嵌套的層次越多,代碼通常越慢。
2.注意那些運行速度慢的數學操作,如平方根、三角函數以及除加減之外的其他運算。
3.減小關鍵數據結構的大小。
4.選擇經常使用的變數的精度(float、double、int、uint8等)。
5.避免無用的運算。
6.簡化數學方程式。
工作原理
第一步要檢查循環,因為循環中的代碼將會執行多次,如果有循環嵌套,那麼執行的次數又會大幅上升。那些被反覆執行的代碼是性能優化的過程中需要重點關注的地方。此外,對於R和Matlab等一些特定領域語言,語言的特性導致它們的循環性能很差。在這種情況下,用另一種編程結構(R里的函數編程方式或者Matlab的向量化表達方法)代替循環會帶來性能的提升。
在第二步中,對於計算密集型任務,通過定位計算緩慢的數學操作,通常能發現顯著提高執行速度的機會。自然界中存在大量的現象需要用平方根來解釋,如常見的計算n維空間內兩點間歐氏距離。不幸的是,求平方根的操作開銷很大,相比於基本的加減乘除以及邏輯判斷,求平方根的操作要慢很多。而乘除相比於加減和邏輯判斷也要慢很多。
一般來說,你手上的T1-85或者HP 48G計算器上,除了加、減、乘、除按鈕,其他數學函數都要遠遠慢於這4個基本操作。這就意味著你必須考慮如下函數。
- cos:餘弦
- sin:正弦
- tan:正切
- acos:反餘弦
- asin:反正弦
- atan:反正切
- exp:冪運算
- pow:以10為底的對數
- ln:自然對數
- cosh:雙曲餘弦
- sinh:雙曲正弦
- tanh:雙曲正切
- acosh:反雙曲餘弦
- asinh:反雙曲正弦
- atanh:反雙曲正切
之前計算兩點間距離的代碼中大量使用了條件判斷語句,如下面的代碼。
if distance_between(point1, point2) > 1.0 ...n
或者將代碼寫得更直接一些:
if square_root( (x1-x2)^2 + (y1-y2)^2 ) > 1.0 ...n
這時優化的機會就出現了:如果在不等式兩端都做平方,那麼開銷昂貴的求平方根方法就可以被去除了。但是需要考慮平方根的一些特性,以及在1、?1和0上的平方操作。平方的過程中,負數會變成正數,小數會更接近零。當處理距離和其他一些物理量時,負數通常都不是一個問題。
作為一個通用的法則,數據結構越小,計算速度越快。越小的數據結構,越能很好地利用分層的存儲結構,數據越接近CPU。理想情況下,所有的計算數據都已經存儲在寄存器上,無須從L1、L2甚至更低的緩存中獲取,更不要說去系統內存中獲取,否則會導致速度明顯下降。而面向對象的語言通常在這方面有缺陷。每個數據結構都必須綁定很多無須用到的方法,導致數據結構變大,以至於不得不在每次迭代過程中從系統內存中獲取數據。
從技術上來說,第四步可以減小數據的體積,但是這也需要針對具體情況來分析。一些語言默認設置整數和浮點數都為64位。對於一些整數來說,並不需要64位這麼大的空間。事實上,大部分情況下,8位的整數就可以滿足一般需求。有些時候不需要表示負數時,無符號整數是一個更優的選擇。
很多浮點數的計算事實上不需要如此高的精度。16位的整數可不可以?32位的整數可不可以?通過這種方式可以將數據體積降為原來的二分之一或者四分之一,並帶來明顯的性能提升。
對於第五步,尋找那些可以在分支中執行的代碼,尤其是需要大量計算的代碼。如果一個昂貴的運算並不是總要去執行,那麼把它放入一個分支中,只在必要的時候運行。
在編寫計算複雜的代碼時,大部分代碼都寫得很簡單明了,使得作者可以一次就得到正確的結果。這就意味著可能會有一些不必要的複雜計算、速度慢的代碼存在,尤其是在條件判斷附近。如果你使用的是編譯型語言,編譯器可能會對代碼進行重新排列,但是最好還是不要過於依賴它(動態語言和JVM上運行的語言,在運行期很難判斷它們究竟會做什麼)。
對於第六步,大部分數學表達式寫出來都是給人看的,而不是為了讓現代計算機軟體和硬體可以很好地運行。簡單修改一下等式,去除一些項或者改寫一些項都會帶來性能的提升。例如,將乘法轉為加法,除法轉為乘法。對於那些需要執行上億的操作來說,這些小小的改動會帶來時間上巨大的節省。加減預算要快於乘法,而乘法又要快於除法。
通讀代碼
我們將要優化的是一段計算分子可接觸表面積(Accessible Surface Area,ASA)的 Python代碼。ASA量化了分子可以被溶液所接觸的表面面積,這是一個在生物和生物化學中廣泛使用的量值。對於本小節來說,你不需要對ASA有十分深入的了解。如果你對此很有興趣,強烈推薦你看一下Bosco K.Ho關於ASA的博客和代碼。他也是本小節中原始代碼的作者。代碼出於清晰和正確的目的,並沒有過多考量性能。
出於優化的目的,代碼將被集成在一個網頁應用中。當用戶上傳數據時,分子的ASA將會被計算。由於所有計算過程都是同步的,代碼執行時間越長,用戶等待時間越久。
本小節,我們將閱讀代碼 asa.py源文件中的核心部分,對代碼進行一個大概的了解,並發現潛在的性能瓶頸。
準備工作
在文本編輯器或者IDE中打開asa.py文件。這個文件中包含的代碼可以在本書的代碼庫中找到。
處理流程
下面的步驟將會帶你瀏覽我們將要優化的代碼。工作原理部分將詳細介紹代碼是如何工作的。
1.在文本編輯器中瀏覽asa.py。注意哪些包被導入、哪些函數被定義,閱讀代碼中的注釋。
2.移動到main()函數,瀏覽之後一連串的函數調用。思考main()函數在asa.py中的作用是什麼。
3.深入閱讀每個函數,從最重要的calculate_asa函數開始。
工作原理
asa.py的main()函數處理包含需要被分析的分子信息的文件及其他命令行參數。當利用 molecule.py中的方法導入分子結構後,再調用calculate_asa函數來處理之後的所有計算。
在文件的頂端,找到第一個函數generate_sphere_points(),這個函數可以計算在指定半徑範圍內的等距點的個數,並將等距點以三元組列表的形式返回。
該函數利用螺旋黃金分割演算法來生成平面上的等距點。此外,還有幾種不同的方法來處理同樣的問題(在參考資料一節會給出相應鏈接)。點的個數是這個函數唯一的參數,這些點用來表示一個表面。越多的點數,越能精確地表現這個表面。如果函數接受無數個點,那麼就可以完美地表示這個表面。
讓我們從一些細節入手性能優化。點的列表在初始化時為空,每輪迭代都會在尾部插入新的點。在快速寫代碼的過程中,體積不斷增長的數據結構通常會導致嚴重的問題。
其次,range(int(n))產生了一個有int(n)個元素的列表,元素的值為0至int(n) ? 1。這種情況下,需要在內存中分配整個擁有n個元素列表的內存空間。當n很大時,需要消耗大量時間進行內存分配。最好利用生成器的方式完成此類工作,比如用xrange函數代替range函數。xrange只需要分配一個xrange對象的內存空間,並且只在真正需要一個值的時候生成一個數字。此外,xrange是用純C語言實現的。
這些是針對3.0版本前的Python。3.0版本後,range實現為一個迭代器。
最後也是最重要的一件事是,Python的for循環對性能消耗很大。我們希望能夠把循環從解釋器中移除,利用編譯後的C代碼來實現。
find_neighbor_indices函數接收3個參數。
- atoms:分子中的原子列表;
- probe:探針的距離,通常設為1.4;
- k:我們需要為k原子找到鄰近原子。
這個函數返回在特定原子探針距離內的原子索引的列表。
代碼中有一個遍歷所有索引的循環。在循環內部,neighbor_indices變數隨迭代不斷增長,vector3d.py中的pos_distance函數不斷被調用。在函數中,我們看到首先計算兩點p1和p2的距離的平方,然後取平方根的操作,如下面的代碼所示。
def pos_distance(p1, p2):n return math.sqrt(pos_distance_sq(p2, p1))nnn def pos_distance_sq(p1, p2):n x = p1.x - p2.xn y = p1.y - p2.yn z = p1.z - p2.zn return x*x + y*y + z*z;n
注意,math.sqrt函數是個計算開銷十分大的函數。
calculate_asa (atoms, probe, n_sphere_point=960)函數是主要的工作函數,用來處理和協調相關的計算。它接受3個參數。
- atoms:分子中的原子列表;
- probe:探針的距離,通常設為1.4;
- n_sphere_point:用來近似表示一個表面所用到的等距點的個數(越多的等距點表示越精準,也越耗時)。
最重要的一個參數是用來計算ASA的原子列表。每個原子都有x、y、z三個坐標和一個相關的半徑。Atom類在molecule.py文件中被定義。
函數一開始通過generate_sphere_points生成一組表面的點並初始化空列表。接下來是一個三層循環,最外層的循環遍歷原子列表中的所有原子,對於一個典型的分子來說可能會有上百甚至上千個原子。在這一層for循環中,對於每個原子調用find_neighbor_indices來生成最鄰近的原子列表。
接下來進入第二層循環,遍歷sphere_points中的每一個點。如果使用默認的960個節點,那麼循環將有960次迭代。對於平面等距點結合中的每一個點,我們加入當前原子的中心坐標。
然後就是最內層循環,遍歷所有的鄰近原子。對於每個鄰近原子,我們比較test_point到鄰近原子中心的距離。如果兩點之間在一個特定的距離內,我們就認為test_point不能被溶劑分子接觸。
用另一種方式來解釋,內部兩層循環利用預先生成好的表面等距點生成測試原子。然後檢查每個點,看看是否存在有一個鄰近原子在指定的距離內。如果存在,那麼表面上的這個點就被認為是不可被接觸的。
原子的可接觸區域即為被鄰近節點所阻斷的原子周圍表面部分。
三層嵌套循環和大量的計算意味著有很大的性能提升空間。
參考資料
- 計算 ASA 的相關文章參見 http://boscoh.com/protein/calculating-the-solvent-accessible- surfacearea-asa.html。
- 最早的ASA計算相關的論文:Environment and exposure to solvent of protein atoms. Lysozyme and insulin, A. Shrake, J.A. Rupley. J Mol Biol 79 (2): 351–71. doi:10.1016/0022-2836(73)90011-9。
本文節選自《數據科學實戰手冊》(R+Python)
這本書是基於R和Python的數據科學項目案例集錦,內容涵蓋了基於數據科學的所有要素,包括數據採集、處理、清洗、分析、建模、可視化以及數據產品的搭建。案例包含了汽車數據分析、股票市場建模、社交網路分析、推薦系統、地理信息分析,以及Python代碼的計算優化。通過手把手的案例解析,令讀者知其然並知其所以然。
業界的數據分析師、數據挖掘工程師、數據科學家都可以讀一讀。想要了解實際工作中如何用數據產生價值的在校學生,或者對數據科學感興趣的人也值得一讀。
從本書中你將學會什麼?
- 了解數據科學的路徑,並且幫助Mac、Windows和Linux操作系統的讀者恰當地搭建數據科學環境。
對汽車數據進行分析和可視化,從中發現不同時間燃料效率的變化趨勢和模式。
模擬美式橄欖球比賽數據(R)
讀者展示如何搭建自己的選股系統,並且使用移動平均法分析股票歷史數據。
就業數據的可視化探索(R)。向讀者展示如何從勞動統計局獲取僱傭和收入數據,並且用R對不同水平的數據進行空間分析。
運用稅務數據進行應用導向的數據分析(Python)。向讀者展示如何使用Python將自己的分析從一次性臨時的工作轉變為可復用的產品化的代碼。這些工作都是基於一份收入數據展開。
運用汽車數據進行可視化分析(Python)這裡使用的是強大的編程語言Python。
社交網路分析(Python),向讀者展示如何建立、可視化和分析社交網路。
大規模電影推薦(Python)。告訴你如何用Python搭建電影推薦系統。
獲取和定位Twitter數據(Python)。向讀者展示如何調用Twitter的API獲取Twitter用戶數據,並繪製用戶信息中包含的地理信息數據。
利用NumPy和SciPy優化數值計算(Python)。帶領讀者領略如何優化Python代碼,從而在處理大數據集時節省時間和金錢。
推薦閱讀:
※什麼使得一個預測模型可被解釋?
※面試坑殺新人指南,第一篇:銷售波動
※成為頂級的數據分析師,要花多少錢?
※快訊 | 5月份新包推薦
※計算廣告訓練與平滑思想(上)