想要讓程式跑得更快能有什麼方式呢?一個看似笑話卻也是作法之一的方式是「什麼事都不要做」,等著下一代處理器上市,然後,跑一下評測(Profiling)工具,接著就可以看到底改進了多少。
不過,就算是這個方式,開發者總也得知道什麼是評測工具,以及目前有哪些類型的評測工具可以使用。
在知道能有多快之前
坦白說,對於重視自我能力提升的開發者而言,接觸一門語言或技術越久,多半寫出的程式效能也不會太差,因為他對語言或技術中的基本元素越是如數家珍,越是清楚這些基本元素的特性,包括對效能的影響,因而在撰寫程式時,自然而然就會採用適當的元素,避免對效能產生不良的影響。
有的人會持反對意見,通常就拿出了「過早最佳化是萬惡根源」這面大旗,然而Donald Ervin Knuth實際上談到的是「97%的情況下,過早最佳化是萬惡根源。然而,我們不該放棄那關鍵的3%。」在網路上搜尋一下效能分析、效能調校之類的文章,或者尋找相關書籍,很容易就能發現,不少調整效能的方式,都是與語言或技術本身相關的基本元素。
談到效能,實際上無論任何程式,都受到三種限制,CPU的速度、記憶體,以及輸入輸出。語言或技術中的基本元素,對這三者可能會有著各自不同的影響,有時甚至只是版本上差異,也會有顯著的影響。
舉例來說,對Java開發者來說,HashMap是再基本不過的元素,然而,在HashMap的容量增長下,JDK8與JDK7的get()相比,時間複雜度分別是O(logn)與O(n),這是因為兩個版本的內部演算實作不同,在內部的桶(Bucket)容量過大時,JDK8的實作會有一個專用的紅黑樹結構來作為替代。
這類語言技術基本元素的特性,會不會影響程式的效能,當然也是得視程式的實際需求而定,也許開發者的應用程式實際執行時,並不會產生大容量的HashMap,這代表著對應用程式需求的瞭解,以及語言技術的使用經驗,能夠告訴開發者,在程式一開始的撰寫就該怎麼做,而又不至於落入萬惡的97%根源,若有能力進行關鍵的3%,也不影響程式的維護性,那就做吧!接下來,若能加上個評測工具,就有個堅強的後盾了。
簡單的計時工具
不同的評測工具有著不同的分析方式,最簡單的計時機制就是在程式中安插程式碼,量測目標程式執行的起始時間與結束時間,然後透過print之類的陳述,將執行前後的時間差顯示出來,這種方式通常用在計算密集式的程式片段或函式呼叫,以便粗略瞭解耗費的整體時間,像這類的方式基本上稱為Instrumentation,安插程式的方式,實際上,視評測工具而有所不同,有些高階的評測工具,則可以在執行時,將Instrumentation安插至被載入記憶體的可執行程式碼中。
若語言本身內建評測工具,都會提供簡單的模組來進行這類計時機制,像是Python的timeit,建議使用這類內建模組,或者是使用第三方程式庫,而不是自行撰寫。在《Dive Into Python》中,也提到「在最佳化Python程式碼時,最重要事是知道:不要自己撰寫計時函式。」
舉例來說,因為平臺本身的一些變數,每次的計時可能得到不同的結果,通常得重複執行相當次數,從中求得穩定的結果;另外,語言本身可能就有些技術上的變因,像是Python直譯器本身對方法的的查找是否作了快取,或者是垃圾回收機制是否停用之類的問題——一個事實是,timeit會停用垃圾收集機制,以避免垃圾回收的影響。如果語言內建計時機制,就算開發者曾經在其他地方看過簡單的介紹,使用前也應該詳閱它的文件,瞭解可以指定的參數,從中瞭解它做了什麼事,以及可能遇到的陷阱。
抓出最慢的傢伙
簡單的計時機制,可用來測試程式片段或者是某個函式呼叫,舉例來說,在Python中,若不確定函式的實作要使用tuple比較快,或者是list比較快,那麼就可以使用timeit做個測試;然而,有時必須在數個函式呼叫之間,找出效能瓶頸來加以改善,畢竟如果改某個地方,程式完成請求只能加快一秒,而改另一個地方,程式卻能加快十秒,那麼後者的改善,顯然效益要來得多。
以Python來說,這類的功能可以透過cProfile模組來達成,通常會搭配pstats來對取得的數據進行排序,通常會令我們感興趣的資料是累積時間(cumulative time, cumtime),這包括了花費在函式本身,以及所有子函式的時間,可以馬上知道哪個部份最為緩慢,也可以看看花在函式本身的總時間(total time, tottime),總時間不包含子函式呼叫,因此若函式本身的總時間花得不多,那麼大部份時間就可能花費在子函式。
函式本身可能被呼叫數次,因而造成cumtime或tottime的有較大的數字,如果在意的是單次呼叫的效能,可以看看cumtime與tottime的percall資料,這會是將cumtime與tottime分別除於呼叫次數(ncalls)的結果,根據這類的資訊,開發者可以決定減少呼叫次數,或者是改進函式本身的演算來試著增進效能。
評測工具本身畢竟本身也是個程式,執行時需要佔據一些系統資源,或者因為評測工具本身設計時技術上的問題,帶來了一些額外的負擔,有些評測工具可以進行校準(calibrate),以便在效能量測之後,能將相關的負擔捨去,例如Python的profiler模組,因為是使用純粹的Python撰寫,負擔上比cProfile來得大,為此profiler模組的Profiler實例,提供了calibrate()方法可用來獲得平臺上的校準常數,以便進一步將獲得的結果,設定為Profiler實例的bias值來作為校準時的捨去值。
空間與時間的交錯
相對於時間的問題,空間的問題比較少人在討論,畢竟現在記憶體越做越大,許多開發者很少在乎空間的問題,更多的情況下,還會有空間換時間的實作方式,像是使用各式的物件池、快取等就是個例子,將建立時需要昂貴運算的資源,設計成可重複使用,因此,通常會是實務效能調校時一個考量的方向。
不過,確實會有因空間而引發的效能問題,記憶體洩漏就是一個情況,此時必須有個空間的評測工具,像是Python中有memory_profiler,可以在原始碼一行一行的基礎上,量測記憶體的使用狀況(時間上的逐行方案則有line_profiler),有時必須要知道,哪種物件耗用了最多的記憶體,像是Python中可以使用heapy來獲得這類資訊。
空間問題與時間問題,基本上是一體的兩面,在現在程式語言多半具有垃圾回收機制的情況下,會讓問題更加複雜,因為,就算沒有記憶體洩漏之類的問題,隨著應用程式運行時間越長,記憶體中的物件越多,垃圾回收時耗費的時間也會成為效能問題之一。
在Java的領域很重視這類問題,有些評測工具可以觀察記憶體中不同世代(Generation)之狀況,亦有不同的演算法可供不同應用程式類型選擇。Python也是採分代回收,可透過gc模組來取得一些資訊,或者對不同世代做些控制。
效能評測工具基本上都不困難,也不應該因難,畢竟效能分析已經夠困難了,沒有理由使用一個困難的工具來做這件事,在試著效能調校之前,先找出能理解、易使用的評測工具是最重要的,如果太複雜,就別去使用它!