太快了,太变态了:什么会影响Java中的方法调用性能?
那么這是怎么回事?
讓我們從一個簡短的故事開始。 幾周前,我提議對Java核心libs郵件列表進行更改 ,以覆蓋當前final一些方法。 這刺激了一些討論主題-其中之一是其中一個性能回歸通過采取這是一個方法被引入的程度final免遭停止它final 。
我對是否會出現性能下降有一些想法,但是我將這些想法放在一邊,試圖詢問是否有關于此主題的合理基準。 不幸的是我找不到任何東西。 這并不是說它們不存在,也沒有其他人沒有對此情況進行調查,但是我沒有看到任何經過公共同行評審的代碼。 所以–是時候寫一些基準了。
標桿管理方法
因此,我決定使用功能強大的JMH框架來匯總這些基準。 如果您不相信框架會幫助您獲得準確的基準測試結果,那么您應該看一下編寫框架的Aleksey Shipilev的演講 ,或者Nitsan Wakart的非常酷的博客文章,其中解釋了它如何提供幫助。
就我而言,我想了解是什么影響了方法調用的性能。 我決定嘗試各種不同的方法調用并衡量成本。 通過設置一組基準并一次僅更改一個因素,我們可以單獨排除或了解不同因素或因素組合如何影響方法調用成本。
內聯
讓我們壓縮這些方法的調用方法。
同時,最明顯的影響因素是根本沒有方法調用! 方法調用的實際成本有可能完全被編譯器優化。 從廣義上講,有兩種降低通話成本的方法。 一種是直接內聯方法本身,另一種是使用內聯緩存。 不用擔心-這些是非常簡單的概念,但是需要引入一些術語。 假設我們有一個名為Foo的類,它定義了一個名為bar的方法。
class Foo {void bar() { ... } }我們可以通過編寫如下代碼來調用bar方法:
Foo foo = new Foo(); foo.bar();這里重要的是實際調用bar的位置– foo.bar() –稱為callsite 。 當我們說一個方法被“內聯”時,這意味著該方法的主體已被取而代之以進入方法,而不是方法調用。 對于由許多小方法組成的程序(我認為是一個適當分解的程序),內聯可以導致明顯的加速。 這是因為該程序并沒有花費大部分時間來調用方法,而是沒有實際執行工作! 通過使用CompilerControl批注,我們可以控制方法是否在JMH中內聯。 稍后我們將回到內聯緩存的概念。
層次深度和覆蓋方法
父母會放慢孩子的速度嗎?
如果我們選擇從方法中刪除final關鍵字,則意味著我們將能夠覆蓋它。 因此,這是我們需要考慮的另一個因素。 因此,我采用了方法并在類層次結構的不同級別上調用了它們,并且還具有在層次結構的不同級別上被覆蓋的方法。 這使我能夠了解或消除深層次的層次結構如何影響成本。
多態性
動物:如何描述任何面向對象的概念。
當我較早提到呼叫站點的想法時,我偷偷地避免了一個相當重要的問題。 由于可以在子類中覆蓋非final方法,因此我們的調用站點final可能會調用不同的方法。 因此,也許我傳入了Foo或它的孩子Baz,它也實現了bar()。 您的編譯器如何知道要調用的方法? 默認情況下,方法是Java中的虛擬(可重寫)方法,它必須為每個調用在稱為vtable的表中查找正確的方法。 這非常慢,因此優化編譯器總是試圖減少所涉及的查找成本。 我們前面提到的一種方法是內聯,如果您的編譯器可以證明在給定的調用站點只能調用一種方法,則該方法非常有用。 這稱為單態呼叫站點。
不幸的是,證明呼叫站點是單態性所需的許多時間分析最終可能是不切實際的。 JIT編譯器傾向于采用另一種方法來分析在調用站點上調用的類型,并猜測如果該調用站點在前N個調用中是單態的,則基于它始終將是單態的假設,值得進行推測性優化。 這種推測性優化通常是正確的,但是由于并不總是正確的,因此編譯器需要在方法調用之前注入防護以檢查方法的類型。
不過,單態調用站點并不是我們要優化的唯一情況。 許多調用站點被稱為雙態的 -可以調用兩種方法。 您仍然可以內聯雙態呼叫站點,方法是使用保護代碼檢查要調用的實現,然后跳轉到該實現。 這仍然比完整方法調用便宜。 也可以使用內聯緩存來優化這種情況。 內聯緩存實際上并不將方法主體內聯到調用站點中,但它具有專門的跳轉表,其作用類似于完整vtable查找上的緩存。 熱點JIT編譯器支持雙態內聯高速緩存,并聲明具有3個或更多可能實現的任何呼叫站點都是megamorphic的 。
這為我們劃分了3種調用情況,以進行基準測試:單態,雙態和超態。
結果
讓我們對結果進行分組,以便更輕松地從樹木中查看木材。我介紹了原始數字以及圍繞它們的一些分析。 實際的數量/成本并不是那么重要。 有趣的是,不同類型的方法調用之間的比率以及相關的錯誤率很低。 最快和最慢之間存在很大的差異– 6.26倍。 實際上,由于與測量空方法的時間相關的開銷,差異可能更大。
這些基準的源代碼可在github上找到 。 為了避免混淆,并沒有全部顯示結果。 最后,多態基準來自運行PolymorphicBenchmark ,而其他基準來自JavaFinalBenchmark
簡單的呼叫網站
Benchmark Mode Samples Mean Mean error Units c.i.j.JavaFinalBenchmark.finalInvoke avgt 25 2.606 0.007 ns/op c.i.j.JavaFinalBenchmark.virtualInvoke avgt 25 2.598 0.008 ns/op c.i.j.JavaFinalBenchmark.alwaysOverriddenMethod avgt 25 2.609 0.006 ns/op我們的第一組結果比較了虛擬方法, final方法和層次結構較深且被覆蓋的方法的調用成本。 請注意,在所有這些情況下,我們都強制編譯器不內聯方法。 正如我們所看到的,時間之間的差異很小,而且我們的平均錯誤率表明它并不重要。 因此,我們可以得出結論,僅添加final關鍵字并不會大大提高方法調用的性能。 覆蓋該方法似乎也沒有太大區別。
內聯簡單的呼叫站點
Benchmark Mode Samples Mean Mean error Units c.i.j.JavaFinalBenchmark.inlinableFinalInvoke avgt 25 0.782 0.003 ns/op c.i.j.JavaFinalBenchmark.inlinableVirtualInvoke avgt 25 0.780 0.002 ns/op c.i.j.JavaFinalBenchmark.inlinableAlwaysOverriddenMethod avgt 25 1.393 0.060 ns/op現在,我們采用了相同的三種情況,并刪除了內聯限制。 同樣, final和虛擬方法調用的結束時間彼此相似。 它們比非內聯情況快大約4倍,我將其歸結為內聯本身。 在此始終被覆蓋的方法調用最終在兩者之間。 我懷疑這是因為方法本身具有多個可能的子類實現,因此編譯器需要插入類型保護。 上面在“多態”下對此進行了詳細解釋。
類等級沖擊
Benchmark Mode Samples Mean Mean error Units c.i.j.JavaFinalBenchmark.parentMethod1 avgt 25 2.600 0.008 ns/op c.i.j.JavaFinalBenchmark.parentMethod2 avgt 25 2.596 0.007 ns/op c.i.j.JavaFinalBenchmark.parentMethod3 avgt 25 2.598 0.006 ns/op c.i.j.JavaFinalBenchmark.parentMethod4 avgt 25 2.601 0.006 ns/op c.i.j.JavaFinalBenchmark.inlinableParentMethod1 avgt 25 1.373 0.006 ns/op c.i.j.JavaFinalBenchmark.inlinableParentMethod2 avgt 25 1.368 0.004 ns/op c.i.j.JavaFinalBenchmark.inlinableParentMethod3 avgt 25 1.371 0.004 ns/op c.i.j.JavaFinalBenchmark.inlinableParentMethod4 avgt 25 1.371 0.005 ns/op哇–這是很多方法! 每個編號的方法調用(1-4)表示調用方法的類層次結構有多深。 所以parentMethod4意味著我們調用了在該類的第4個父級上聲明的方法。 如果看一下數字,則1和4之間的差異很小。因此,我們可以得出結論,層次深度沒有區別。 可內聯的案例都遵循相同的模式:層次深度沒有區別。 我們的inlineable方法性能與inlinableAlwaysOverriddenMethod相當,但比inlinableVirtualInvoke慢。 我再次將其歸結為所使用的類型防護。 JIT編譯器可以對方法進行概要分析,以找出僅內聯的一種方法,但無法證明這是永遠存在的。
類層次對
Benchmark Mode Samples Mean Mean error Units c.i.j.JavaFinalBenchmark.parentFinalMethod1 avgt 25 2.598 0.007 ns/op c.i.j.JavaFinalBenchmark.parentFinalMethod2 avgt 25 2.596 0.007 ns/op c.i.j.JavaFinalBenchmark.parentFinalMethod3 avgt 25 2.640 0.135 ns/op c.i.j.JavaFinalBenchmark.parentFinalMethod4 avgt 25 2.601 0.009 ns/op c.i.j.JavaFinalBenchmark.inlinableParentFinalMethod1 avgt 25 1.373 0.004 ns/op c.i.j.JavaFinalBenchmark.inlinableParentFinalMethod2 avgt 25 1.375 0.016 ns/op c.i.j.JavaFinalBenchmark.inlinableParentFinalMethod3 avgt 25 1.369 0.005 ns/op c.i.j.JavaFinalBenchmark.inlinableParentFinalMethod4 avgt 25 1.371 0.003 ns/op這遵循與上述相同的模式final關鍵字似乎沒有什么區別。 我會認為這是可能在這里,從理論上說,對于inlinableParentFinalMethod4來加以證明inlineable沒有型后衛,但它不會出現這種情況。
多態性
Monomorphic: 2.816 +- 0.056 ns/op Bimorphic: 3.258 +- 0.195 ns/op Megamorphic: 4.896 +- 0.017 ns/op Inlinable Monomorphic: 1.555 +- 0.007 ns/op Inlinable Bimorphic: 1.555 +- 0.004 ns/op Inlinable Megamorphic: 4.278 +- 0.013 ns/op最后,我們來談談多態調度的情況。 單態調用成本與上面的常規虛擬調用成本大致相同。 由于我們需要在較大的vtable上進行查找,因此隨著雙態和多態情況的顯示,它們變得更慢。 一旦啟用內聯類型分析,我們的單態和雙態調用站點就會降低我們的“內聯守衛”方法調用的成本。 因此與類層次結構的情況類似,只是速度稍慢一些。 大形情況仍然非常緩慢。 請記住,我們這里沒有告訴熱點防止內聯,只是它沒有為比雙態更復雜的調用站點實現多態內聯緩存。
我們學到了什么?
我認為值得一提的是,有很多人沒有表現心理模型來說明花費時間不同的不同類型的方法調用,還有很多人知道他們花費的時間不同,但實際上并沒有非常正確。 我知道我以前去過那里,做了各種各樣的錯誤假設。 因此,我希望這項調查對人們有所幫助。 這是我很樂意支持的聲明摘要。
- 最快和最慢的方法調用類型之間有很大的不同。
- 在實踐中,添加或刪除final關鍵字并不會真正影響性能,但是,如果您隨后重構層次結構,事情可能會開始放慢速度。
- 更深的類層次結構對呼叫性能沒有真正的影響。
- 單態調用比雙態調用更快。
- 雙態調用比大形調用快。
- 在概要分析(但不是可證明)的情況下,我們看到的類型防護在單態調用站點上確實使速度放慢了很多。
我會說類型保護程序的成本是我個人的“重大啟示”。 這是我鮮為人知的話題,經常被認為是無關緊要的。
注意事項和進一步工作
當然,這不是主題領域的最終決定!
- 該博客僅關注與方法調用性能有關的類型相關因素。 我沒有提到的一個因素是由于主體大小或調用堆棧深度而導致的圍繞方法內聯的啟發式方法。 如果您的方法太大,則根本不會內聯,您仍然要為方法調用的費用付費。 編寫小的,易于閱讀的方法的另一個原因。
- 我沒有研究過接口調用如何影響這些情況。 如果您發現這很有趣,那么可以在Mechanical Sympathy博客上研究調用接口的性能。
- 我們在這里完全忽略的一個因素是方法內聯對其他編譯器優化的影響。 當編譯器執行僅考慮一種方法的優化(過程內優化)時,他們實際上希望獲得盡可能多的信息以進行有效的優化。 內聯的局限性可以大大縮小其他優化必須使用的范圍。
- 將說明直接附加到匯編級別,以深入了解該問題。
也許這些是將來博客文章的主題。
翻譯自: https://www.javacodegeeks.com/2014/05/too-fast-too-megamorphic-what-influences-method-call-performance-in-java.html
總結
以上是生活随笔為你收集整理的太快了,太变态了:什么会影响Java中的方法调用性能?的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Flume:使用Apache Flume
- 下一篇: 怎么让笔记本强制重启电脑(如何将笔记本电