重構是在不改變程式可觀察行為的情況下,對既有的程式碼進行調整,提高程式碼的可理解性;效能最佳化(optimization)也是在不改變程式可觀察行為的情況下,對既有的程式碼進行修改,然而,其目的卻是另外一個:讓程式的運行在時間或空間上更有效率。

背道而馳的兩個活動?

在〈IsOptimizationRefactoring〉中,Martin Fowler就談到了,重構與效能最佳化是兩個不同的概念;甚至在許多情況下,兩個活動後的程式碼,結果會是背道而馳,效率與可讀性之間像是一種權衡,有一方增加,另一方就看似減少。

例如,為了讓程式運行更有效率,程式碼往往會變得更為複雜,像是:在採用時間或空間的權衡上,有著更複雜考量的資料結構、實現有著更複雜數學或理論的演算法、撰寫更能發揮底層效率的特定語法等。

相對地,為了讓程式碼更易於理解,常會做出讓(局部)程式碼變慢的修改,就像Martin Fowler的《重構》第1章的簡單示範,為了切分出獨立的子任務,必須將一個迴圈能做完的事,拆分使用數個迴圈來完成,為了避免函式過於冗長,必須將一個函式能做的事情,抽取為數個函式,為了讓物件不致於臃腫,必須將一個物件能擔負的職責,細分為數個不同職責的物件等。

Donald Ervin Knuth也講過:「過早最佳化是萬惡根源」,比起效能,可讀性更為重要,對吧?不對!先前專欄〈不只靠青春飯的程式人〉就談過了,「過早最佳化是萬惡根源」這句話有被濫用的情況;另一方面,Martin Fowler在《重構》也談過,他並不贊成為了提高設計的純潔性而忽略了程式的效能。

尋找3%至10%效能關鍵?

Knuth談到的是「97%的情況下,過早最佳化是萬惡根源」,Martin Fowler談到的是「90%的最佳化工作都是白費工」,尋找那3%到10%的效能關鍵、加以調整,對於效能最佳化才有真正顯著的助益,而這不是憑空想像,必須有適當的評測工具,透過具體的資料分析,來找出那關鍵的3%至10%。

然後呢?效能瓶頸若源自於程式面,終究還是必須回到程式上的修改,然而,粒度是個問題,如果程式碼一團混亂,我們有辦法看出造成瓶頸的這段程式碼,問題是出在資料結構、演算法、不合理的實現方式、無用冗餘的程式碼,或甚至只是臭蟲造成的嗎?

更進一步地,如果真的在一團混亂的程式碼找出問題了,有信心修改嗎?是的!應該先建立測試,以確保修改後功能正常,不過,面對一團流程、狀態上下相依的邏輯泥塊,建立測試絕對是個大挑戰──別的不說,就算只是想建立一個大致的測試覆蓋率,都會是個問題。

Martin Fowler曾經談到:「雖然重構可能使軟體運行更慢,但也使軟體的效能最佳化更容易」,這幾句話看似前後矛盾,然而,Martin Fowler談到箇中的祕密就在於,在進入效能最佳化的階段前,不對效能過於關注,先去建立易於理解、可以最佳化的程式。

不過,正如〈不只靠青春飯的程式人〉談過的,這並非要開發者撰寫程式時,完全不考量效能,若經驗上已知某演算、資料結構或語言特性可有效率地解決問題,也不影響可讀性,就應該使用,所謂「不對效能過於關注」的意思是,在可讀性與效能無法兼得時,就以可讀性優先,等到進入效能最佳化階段後,再來遵循特定的流程進行調整,以取得足夠的效率。

進入效能最佳化階段

易於理解的程式碼,每個子任務的粒度會比較小,而透過評測工具進行解析時,往往也能找出更小範疇的效能瓶頸,在動手最佳化之前,建立測試也相對地容易,而真正要動手時,由於程式碼的粒度小,相對而言,在理解瓶頸部份的程式碼做了什麼事情時,也會比較容易。

如果仍然不容易理解,要做的第一件事仍是動手重構,提高可讀性,往往在這個階段,我們就可以從事基本的最佳化,像是對程式中重複、冗餘或浪費資源的部份進行調整,或者找到能套用執行環境原生特性的地方,無論是使用特定語法、函式、惰性、非同步,或甚至平行化等方案。

如果覺得有瓶頸的部分,或許可以透過某些演算法(或資料結構)來加以改善,要做的事情也是先重構,讓演算法的實現成為一個子任務,並讓子任務有個清楚協定,例如,基於某個函式簽署或物件行為,令套用演算法前、後的程式碼,就像是在這類介面底層的不同實作。

這就是Martin Fowler說的,易於理解的程式就是可以最佳化的程式,他在《重構》也談到,在進入效能最佳化階段之後,每個修改也應該是小幅度的,並且通過測試、確認功能正確後,進一步透過評測工具,看看效能有無增益,如果沒有就必須撤回修改,嘗試其他的改進,這種小幅度的修改、測試、評測,必須持續不斷,直到達到效能符合需求為止。

另一方面,開發者在涉及採用其他演算法來改進效能時,往往認為程式就會複雜化,不過,這應該進一步地去思考,複雜化的來源是什麼。

有些時候,演算法本質上的複雜,例如,演算法追求特定效率時必須採用的原理,必須理解特定領域知識或者數學模型等;有時來源與特定語言實現演算法的難易度有關;然而,有時某個演算法看來複雜,純粹就只是開發者在表示或實作演算法時,沒有給予良好的可讀性!

舉例來說,經常我在面對一些演算法的虛擬碼或特定語言實現時,都會倒吸一口氣「天啊!這虛擬碼(程式碼)也太冗長了吧!」這時,我會做的,就是對它進行重構,逐段去找出演算法中的子任務,再將這些部分抽取出來,逐步令演算法變得可讀,這麼一來,在採用自身想使用的語言實現時,也能逐個子任務去實現、建立測試,確認每個子任務的功能符合需求。

這麼一來,在這些子任務都實現、完成之後,往往整個演算法的實現就自然成形了;進一步地,我們在評測效能的時候,演算法的可讀性就能派上用場,也就能依自身的需求,來調整演算法,以便改善效能。

想調整效能?先增加可讀性!

實施重構、令程式碼變得可讀性時,乍看之下,我們可能會覺得(局部)程式碼變慢了,然而現代軟體在開發時,影響效能的原因太多了,採用的語言、執行的環境等都有可能,為了在真正面對效能瓶頸時,能有效地評估、理解、修改,程式碼必須具備可讀性!

事實上,重構與效能最佳化並非背道而馳的兩個活動,雖然重構不一定會直接增加效能,但是,重構改善了程式碼的可讀性,而可讀性高的程式碼,能為效能的調整增加更多可行性!

因此,開發者應時時讓程式碼保持在易於理解的狀態,如果未來想調整效能時,就會易於實施。如果我們手邊的程式碼並不是這麼一回事,先動手重構,設法增加可讀性,這會是效能最佳化前的必行之路!

專欄作者

熱門新聞

Advertisement