方法内联在JVM中有多积极?
IntelliJ IDEA中使用Ctrl + Alt + M 提取方法 。 Ctrl + Alt + M。 這就像選擇一段代碼并按此組合一樣簡單。 Eclipse也有它 。 我討厭冗長的方法。 對于我來說,聞起來太久了:
首先,它具有不可讀的條件。 無關緊要的實現方式,重要的是做什么。 因此,讓我們首先提取它:
public void processOnEndOfDay(Contract c) {if (isOutdated(c)) {priorityHandling(c, OUTDATED_FEE);notifyOutdated(c);log.info("Outdated: {}", c);} else {if(sendNotifications) {notifyPending(c);}log.debug("Pending {}", c);} }private boolean isOutdated(Contract c) {return DateUtils.addDays(c.getCreated(), 7).before(new Date()); }顯然,此方法并不真正屬于這里( F6 –移動實例方法):
public void processOnEndOfDay(Contract c) {if (c.isOutdated()) {priorityHandling(c, OUTDATED_FEE);notifyOutdated(c);log.info("Outdated: {}", c);} else {if(sendNotifications) {notifyPending(c);}log.debug("Pending {}", c);} }注意到不同了嗎? 我的IDE使isOutdated()是Contract的實例方法,聽起來isOutdated() 。 但是我還是不開心。 這種方法發生了太多的事情。 一個分支執行一些與業務相關的priorityHandling() ,一些系統通知和日志記錄。 其他分支執行條件通知和日志記錄。 首先,讓我們將處理過時的合同轉移到單獨的方法中:
public void processOnEndOfDay(Contract c) {if (c.isOutdated()) {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.isOutdated()) {handleOutdated(c);} else {stillPending(c);} }private void handleOutdated(Contract c) {priorityHandling(c, OUTDATED_FEE);notifyOutdated(c);log.info("Outdated: {}", c); }private void stillPending(Contract c) {if(sendNotifications) {notifyPending(c);}log.debug("Pending {}", c); }這個例子有些人為,但實際上我想證明一些不同的東西。 這些天并不經常出現,但是仍然有開發人員擔心提取方法會認為它在運行時速度較慢。 他們無法理解JVM是一款很棒的軟件(它可能遠遠超過Java語言),它內置了許多真正令人驚嘆的運行時優化。 首先,較短的方法更容易推理。 流動更明顯,范圍更短,副作用更明顯。 使用長方法,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可以發現代碼的運行時特征,例如,哪些方法最常執行,哪些虛擬方法只有一個實現等。
- 使用舊Java編譯的.class將在較新的JVM上運行得更快。 您很有可能會更新Java,然后重新編譯源代碼。
讓我們對所有這些假設進行測試。 我寫了一個小程序,標題為“ 有史以來最糟糕的分而治之原則 。 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;}最后,我還提供了一個使用abstract方法和繼承的實現。 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 {@Overridepublic 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);}@Overridepublic 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);}@Overridepublic 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);}@Overridepublic 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);}@Overridepublic 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);}@Overridepublic int add4(int x1, int x2, int x3, int x4) {return add2(x1, x2) + add2(x3, x4);}@Overridepublic int add2(int x1, int x2) {return x1 + x2;}} 在有關@Cacheable開銷的文章發表后,受到一些有趣的讀者意見的鼓勵,我編寫了一個快速基準測試,以比較過度提取的ConcreteAdder和VirtualAdder開銷(以查看虛擬調用開銷)。 結果出乎意料,有些模棱兩可。 我在兩臺機器(藍色和紅色),相同的軟件上運行相同的基準測試,但是第二臺具有更多內核,并且是64位:
詳細環境:
事實證明,在一臺速度較慢的計算機上, JVM決定內聯所有內容。 不僅簡單的private通話,而且虛擬的通話一次。 那怎么可能 很好,JVM發現Adder只有一個子類,因此每個abstract方法只有一個可能的版本。 如果在運行時加載另一個子類(或什至更多子類),則可能會看到性能下降,因為不再可能進行內聯。 但是拋開細節,在此基準測試方法中調用并不便宜,實際上是免費的 ! 方法調用(具有極大的文檔價值,提高了可讀性)僅存在于源代碼和字節碼中。 在運行時,它們被完全消除(內聯)。
我不太理解第二個基準。 看起來機器B的運行速度確實更快,但實際上運行參考SingleMethodCall基準測試的速度更快,但其他機器甚至比A都慢。 也許它決定推遲內聯? 差異是顯著的,但并不是那么大。 同樣,就像優化堆棧跟蹤生成一樣,如果您開始通過手動內聯方法來優化代碼,從而使它們變得更長和復雜,那么您正在解決錯誤的問題。
該基準可在GitHub上獲得 ,以及文章來源 。 我鼓勵您在設置中運行它。 此外,每個拉取請求都是自動在Travis上構建的,因此您可以在同一環境下輕松比較結果。
參考: 方法在JVM中內聯的積極性如何? 來自我們的JCG合作伙伴 Tomasz Nurkiewicz,來自Java和鄰里博客。
翻譯自: https://www.javacodegeeks.com/2013/02/how-aggressive-is-method-inlining-in-jvm.html
總結
以上是生活随笔為你收集整理的方法内联在JVM中有多积极?的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: EasyCriteria 2.0 – J
- 下一篇: 中世纪精致风 RPG 游戏《Mirthw