標籤:
在IntelliJ IDEA裡面Ctrl+Alt+M用來拆分方法。選中一段代碼,敲下這個組合,很easy。Eclipse也用類似的快速鍵,使用 Alt+Shift+M。我討厭長的方法,提起這個以下這種方法我就認為太長了:
public void processOnEndOfDay(Contract c) {if (DateUtils.addDays(c.getCreated(), 7).before(new Date())) {priorityHandling(c, OUTDATED_FEE);notifyOutdated(c);log.info("Outdated: {}", c);} else {if (sendNotifications) {notifyPending(c);}log.debug("Pending {}", c);}}
首先,它有個條件推斷可讀性非常差。先無論它怎麼實現的,它做什麼的才最關鍵。我們先把它拆分出來:
public void processOnEndOfDay(Contract c) {if (isOutDate(c)) {priorityHandling(c, OUTDATED_FEE);notifyOutdated(c);log.info("Outdated: {}", c);} else {if (sendNotifications) {notifyPending(c);}log.debug("Pending {}", c);}}private boolean isOutDate(Contract c) {return DateUtils.addDays(c.getCreated(), 7).before(new Date());}
非常明顯,這種方法不應該放到這裡:
public void processOnEndOfDay(Contract c) {if (c.isOutDate()) {priorityHandling(c, OUTDATED_FEE);notifyOutdated(c);log.info("Outdated: {}", c);} else {if (sendNotifications) {notifyPending(c);}log.debug("Pending {}", c);}}
注意到什麼不同嗎?我的IDE把isOutdated方法改成Contract的執行個體方法了,這才像樣嘛。只是我還是不爽。這種方法做的事太雜了。一個分支在處理業務相關的邏輯priorityHandling,以及發送系統通知和記錄日誌。還有一個分支在則依據推斷條件做系統通知,同一時候記錄日誌。我們先把處理到期合約拆分成一個獨立的方法.
public void processOnEndOfDay(Contract c) {if (c.isOutDate()) {handleOutdated(c);} else {if (sendNotifications) {notifyPending(c);}log.debug("Pending {}", c);}}private void handleOutdated(Contract c) {priorityHandling(c, OUTDATED_FEE);notifyOutdated(c);log.info("Outdated: {}", c);}
有人會認為這樣已經夠好了,只是我認為兩個分支並不正確稱令人紮眼。handleOutdated方法層級更高些,而else分支更偏細節。軟體應該清晰易讀,因此不要把不同層級間的代碼混到一起。這樣我會更愜意:
public void processOnEndOfDay(Contract c) {if (c.isOutDate()) {handleOutdated(c);} else {stillPending(c);}}private void stillPending(Contract c) {if (sendNotifications) {notifyPending(c);}log.debug("Pending {}", c);}private void handleOutdated(Contract c) {priorityHandling(c, OUTDATED_FEE);notifyOutdated(c);log.info("Outdated: {}", c);}
這個範例看起來有點裝,只是事實上我想證明的是還有一個事情。儘管如今不太常見了,只是還是有些開發人員不敢拆分方法,操心這種話影響執行效率。他們不知道JVM事實上是個很棒的軟體(它事實上甩Java語言好幾條街),它內建有很多很令人吃驚的執行時最佳化。首先短方法更利於JVM判斷。流程更明顯,範圍更短,副作用也更明顯。假設是長方法JVM可能直接就跪了。第二個原因則更重要:
方法內聯
假設JVM監測到一些小方法被頻繁的運行,它會把方法的調用替換成方法體本身。比方說以下這個:
private int add4(int x1, int x2, int x3, int x4) {return add2(x1, x2) + add2(x3, x4);}private int add2(int x1, int x2) {return x1 + x2;}
能夠肯定的是執行一段時間後JVM會把add2方法去掉,並把你的代碼翻譯成:
private int add4(int x1, int x2, int x3, int x4) {return x1 + x2 + x3 + x4;}
注意這說的是JVM,而不是編譯器。javac在產生位元組碼的時候是比較保守的,這些工作都扔給JVM來做。事實證明這種設計決策是很明智的:
JVM更清楚執行的目標環境 ,CPU,記憶體,體繫結構,它能夠更積極的進行最佳化。 JVM能夠發現你代碼執行時的特徵,比方,哪個方法被頻繁的執行,哪個虛方法僅僅有一個實現,等等。 舊編譯器編譯的.class在新版本號碼的JVM上能夠擷取更快的執行速度。更新JVM和又一次編譯源碼,你肯定更傾向於後者。
我們對這些如果做下測試。我寫了一個小程式,它有著分治原則的最糟實現的稱號。add128方法須要128個參數而且調用了兩次add64方法——前後兩半各一次。add64也類似,只是它是調用了兩次add32。你猜的沒錯,最後會由add2方法來結束這一切,它是幹苦力活的。有些數字我給省略了,免得亮瞎了你的眼睛:
public class ConcreteAdder { public int add128(int x1, int x2, int x3, int x4, ... more ..., int x127, int x128) { return add64(x1, x2, x3, x4, ... more ..., x63, x64) + add64(x65, x66, x67, x68, ... more ..., x127, x128); } private int add64(int x1, int x2, int x3, int x4, ... more ..., int x63, int x64) { return add32(x1, x2, x3, x4, ... more ..., x31, x32) + add32(x33, x34, x35, x36, ... more ..., x63, x64); } private int add32(int x1, int x2, int x3, int x4, ... more ..., int x31, int x32) { return add16(x1, x2, x3, x4, ... more ..., x15, x16) + add16(x17, x18, x19, x20, ... more ..., x31, x32); } private int add16(int x1, int x2, int x3, int x4, ... more ..., int x15, int x16) { return add8(x1, x2, x3, x4, x5, x6, x7, x8) + add8(x9, x10, x11, x12, x13, x14, x15, x16); } private int add8(int x1, int x2, int x3, int x4, int x5, int x6, int x7, int x8) { return add4(x1, x2, x3, x4) + add4(x5, x6, x7, x8); } private int add4(int x1, int x2, int x3, int x4) { return add2(x1, x2) + add2(x3, x4); } private int add2(int x1, int x2) { return x1 + x2; }}
不難發現,調用add128方法最後一共產生了127個方法調用。太多了。作為參考,以下這有個簡單直接的實現版本號碼:
public class InlineAdder { public int add128n(int x1, int x2, int x3, int x4, ... more ..., int x127, int x128) { return x1 + x2 + x3 + x4 + ... more ... + x127 + x128; } }
最後再來一個使用了抽象類別和繼承的實現版本號碼。127個虛方法調用開銷是很大的。這些方法須要動態分發,因此要求更高,所以無法進行內聯。
public abstract class Adder { public abstract int add128(int x1, int x2, int x3, int x4, ... more ..., int x127, int x128); public abstract int add64(int x1, int x2, int x3, int x4, ... more ..., int x63, int x64); public abstract int add32(int x1, int x2, int x3, int x4, ... more ..., int x31, int x32); public abstract int add16(int x1, int x2, int x3, int x4, ... more ..., int x15, int x16); public abstract int add8(int x1, int x2, int x3, int x4, int x5, int x6, int x7, int x8); public abstract int add4(int x1, int x2, int x3, int x4); public abstract int add2(int x1, int x2);}
另一個實現:
public class VirtualAdder extends Adder { @Override public int add128(int x1, int x2, int x3, int x4, ... more ..., int x128) { return add64(x1, x2, x3, x4, ... more ..., x63, x64) + add64(x65, x66, x67, x68, ... more ..., x127, x128); } @Override public int add64(int x1, int x2, int x3, int x4, ... more ..., int x63, int x64) { return add32(x1, x2, x3, x4, ... more ..., x31, x32) + add32(x33, x34, x35, x36, ... more ..., x63, x64); } @Override public int add32(int x1, int x2, int x3, int x4, ... more ..., int x32) { return add16(x1, x2, x3, x4, ... more ..., x15, x16) + add16(x17, x18, x19, x20, ... more ..., x31, x32); } @Override public int add16(int x1, int x2, int x3, int x4, ... more ..., int x16) { return add8(x1, x2, x3, x4, x5, x6, x7, x8) + add8(x9, x10, x11, x12, x13, x14, x15, x16); } @Override public int add8(int x1, int x2, int x3, int x4, int x5, int x6, int x7, int x8) { return add4(x1, x2, x3, x4) + add4(x5, x6, x7, x8); } @Override public int add4(int x1, int x2, int x3, int x4) { return add2(x1, x2) + add2(x3, x4); } @Override public int add2(int x1, int x2) { return x1 + x2; }}
受到我的還有一篇關於@Cacheable 負載的文章的一些熱心讀者的鼓勵,我寫了個簡單的基準測試來比較這兩個過度分拆的ConcreteAdder和VirtualAdder的負載。結果出人意外,還有點讓人摸不著頭腦。我在兩台機器上做了測試(紅色和藍色的),相同的程式不同的是第二台機器CPU核心數很多其它並且是64位的:
詳細的環境資訊:
看起來慢的機器上JVM更傾向於進行方法內聯。不僅是簡單的私人方法調用的版本號碼,虛方法的版本號碼也一樣。為什麼會這樣?由於JVM發現Adder僅僅有一個子類,也就是說每一個抽象方法都僅僅有一個版本號碼。假設你在執行時載入了還有一個子類(或者很多其它),你會看到效能會直線下降,由於無能再進行內聯了。先無論這個了,從測試中來看,
這些方法的調用並非開銷非常低,是根本就沒有開銷!
方法調用(還有為了可讀性而加的文檔)僅僅存在於你的源碼和編譯後的位元組碼裡,執行時它們全然被清除掉了(內聯了)。
我對第二個結果也不太理解。看起來效能高的機器B執行單個方法調用的時候要快點,另兩個就要慢些。或許它傾向於延遲進行內聯?結果是有些不同,只是差距也不是那麼的大。就像 最佳化棧跟蹤資訊產生 那樣——假設你為了最佳化代碼效能,手動進行內聯,把方法越搞越龐大,越弄越複雜,那你就真的錯了。
ps:64bit 機器之所以執行慢有可能是由於 JVM 內聯的要求的方法長度較長。
文章原文來源於:
http://www.javacodegeeks.com/2013/02/how-aggressive-is-method-inlining-in-jvm.html
http://it.deepinmind.com/java/2014/03/01/JVM的方法內聯.html
深入理解java虛擬機器(十四)正確利用 JVM 的方法內聯