通過優化代碼來降低運作時間是一件十分有意義的工作。然而,大部分情況下,優化是編寫軟體的最後一個步驟,并且通常是在不得不優化的情況下進行的。如果你的代碼可以在能夠接受的時間範圍内運作完成,那麼優化并不是必需的。
鑒于此,為什麼我們還要優化代碼呢?作為資料科學家,我們現在面對的是越來越大規模的資料集,在每個元素上進行的操作可能要重複數十億次才能得到最後的結果。如果代碼品質很低,可能需要花費數天才能完成運作。此外,大部分科學計算和數值軟體是計算密集型而不是網絡密集型的任務。鑒于資料計算中使用的模型也越來越複雜,每一點的性能下降都會産生嚴重的後果。那麼優化代碼還有什麼其他的原因嗎?原因如下。時間就是金錢:如果你是在雲端運作程式,那麼時間就是金錢。額外的每一分鐘處理都需要付費,當程式在數千台機器上的時候更是如此。而且要注意的是,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 ...
或者将代碼寫得更直接一些:
if square_root( (x1-x2)^2 + (y1-y2)^2 ) > 1.0 ...
這時優化的機會就出現了:如果在不等式兩端都做平方,那麼開銷昂貴的求平方根方法就可以被去除了。但是需要考慮平方根的一些特性,以及在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):
return math.sqrt(pos_distance_sq(p2, p1))
def pos_distance_sq(p1, p2):
x = p1.x - p2.x
y = p1.y - p2.y
z = p1.z - p2.z
return x*x + y*y + z*z;
注意,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計算相關的論文: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代碼,進而在處理大資料集時節省時間和金錢。