java 调用 js性能_太快了,太变态了:什么会影响Java中的方法调用性能?
java 調(diào)用 js性能
那么這是怎么回事?
讓我們從一個簡短的故事開始。 幾周前,我提議對Java核心libs郵件列表進行更改 ,以覆蓋當前final一些方法。 這刺激了一些討論主題-其中之一是其中一個性能回歸通過采取這是一個方法被引入的程度final免遭停止它final 。
我對是否會出現(xiàn)性能下降有一些想法,但是我將這些想法放在一邊,試圖詢問是否有關(guān)于此主題的合理基準發(fā)布。 不幸的是我找不到任何東西。 這并不是說它們不存在,也沒有其他人沒有對此情況進行調(diào)查,但是我沒有看到任何經(jīng)過公共同行評審的代碼。 所以–是時候?qū)懸恍┗鶞柿恕?
標桿管理方法
因此,我決定使用功能強大的JMH框架來匯總這些基準。 如果您不相信框架會幫助您獲得準確的基準測試結(jié)果,那么您應(yīng)該看一下編寫框架的Aleksey Shipilev的演講 ,或者Nitsan Wakart的非常酷的博客文章,其中解釋了它如何提供幫助。
就我而言,我想了解是什么影響了方法調(diào)用的性能。 我決定嘗試各種不同的方法調(diào)用并衡量成本。 通過設(shè)置一組基準并一次僅更改一個因素,我們可以單獨排除或了解不同因素或因素組合如何影響方法調(diào)用成本。
內(nèi)聯(lián)
讓我們壓縮這些方法的調(diào)用方法。
同時,最明顯的影響因素是根本沒有方法調(diào)用! 方法調(diào)用的實際成本有可能完全被編譯器優(yōu)化。 從廣義上講,有兩種降低通話成本的方法。 一種是直接內(nèi)聯(lián)方法本身,另一種是使用內(nèi)聯(lián)緩存。 不用擔心-這些是非常簡單的概念,但是需要引入一些術(shù)語。 假設(shè)我們有一個名為Foo的類,它定義了一個名為bar的方法。
class Foo {void bar() { ... } }我們可以通過編寫如下代碼來調(diào)用bar方法:
Foo foo = new Foo(); foo.bar();重要的是實際調(diào)用bar的位置– foo.bar() –稱為callsite 。 當我們說某個方法被“內(nèi)聯(lián)”時,這意味著該方法的主體已被取而代之以進入方法,而不是方法調(diào)用。 對于由許多小方法組成的程序(我認為是一個適當分解的程序),內(nèi)聯(lián)可以顯著提高速度。 這是因為該程序并沒有花費大部分時間來調(diào)用方法,而是沒有實際執(zhí)行工作! 通過使用CompilerControl批注,我們可以控制JMH中是否內(nèi)聯(lián)方法。 稍后我們將回到內(nèi)聯(lián)緩存的概念。
層次深度和覆蓋方法
父母會放慢孩子的速度嗎?
如果我們選擇從方法中刪除final關(guān)鍵字,則意味著我們將能夠覆蓋它。 因此,這是我們需要考慮的另一個因素。 因此,我采用了方法并在類層次結(jié)構(gòu)的不同級別上調(diào)用了它們,并且還具有在層次結(jié)構(gòu)的不同級別上被覆蓋的方法。 這使我能夠理解或消除深層次的層次結(jié)構(gòu)如何影響成本。
多態(tài)性
動物:如何描述任何面向?qū)ο蟮母拍睢?
當我較早提到呼叫站點的想法時,我偷偷地避免了一個相當重要的問題。 由于可以在子類中覆蓋非final方法,因此我們的調(diào)用站點final可能會調(diào)用不同的方法。 因此,也許我傳入了Foo或它的孩子Baz,它也實現(xiàn)了bar()。 您的編譯器如何知道要調(diào)用的方法? 默認情況下,方法是Java中的虛擬(可重寫)方法,它必須為每個調(diào)用在稱為vtable的表中查找正確的方法。 這非常慢,因此優(yōu)化編譯器總是試圖減少所涉及的查找成本。 我們前面提到的一種方法是內(nèi)聯(lián),如果您的編譯器可以證明只能在給定的調(diào)用站點上調(diào)用一種方法,則該方法非常有用。 這稱為單態(tài)呼叫站點。
不幸的是,在很多情況下,證明呼叫站點是單態(tài)的分析最終可能是不切實際的。 JIT編譯器傾向于采用另一種方法來分析在調(diào)用站點上調(diào)用的類型,并猜測如果該調(diào)用站點在前N個調(diào)用中是單態(tài)的,則基于它始終將是單態(tài)的假設(shè),值得進行推測性優(yōu)化。 這種推測性優(yōu)化通常是正確的,但是由于并不總是正確的,因此編譯器需要在方法調(diào)用之前注入防護以檢查方法的類型。
不過,單態(tài)調(diào)用場并不是我們要優(yōu)化的唯一情況。 許多調(diào)用站點被稱為雙態(tài) –有兩種可以調(diào)用的方法。 您仍然可以內(nèi)聯(lián)雙態(tài)呼叫站點,方法是使用保護代碼檢查要調(diào)用的實現(xiàn),然后跳轉(zhuǎn)到該實現(xiàn)。 這仍然比完整方法調(diào)用便宜。 也可以使用內(nèi)聯(lián)緩存來優(yōu)化這種情況。 內(nèi)聯(lián)緩存實際上并不將方法主體內(nèi)聯(lián)到調(diào)用站點中,但是它具有專門的跳轉(zhuǎn)表,其作用類似于完整vtable查找上的緩存。 熱點JIT編譯器支持雙態(tài)內(nèi)聯(lián)高速緩存,并聲明具有3個或更多可能實現(xiàn)的任何呼叫站點都是megamorphic的 。
這將再分為3種調(diào)用情況供我們進行基準測試和研究:單態(tài)情況,雙態(tài)情況和大態(tài)情況。
結(jié)果
讓我們對結(jié)果進行分組,以便更輕松地從樹木中查看木材。我介紹了原始數(shù)字以及圍繞它們的一些分析。 具體的數(shù)字/成本并不是那么重要。 有趣的是,不同類型的方法調(diào)用之間的比率以及相關(guān)的錯誤率很低。 最快和最慢之間存在很大的差異– 6.26倍。 實際上,由于與測量空方法的時間相關(guān)的開銷,差異可能更大。
這些基準的源代碼可在github上找到 。 為了避免混淆,結(jié)果不會全部顯示在一個塊中。 最后,多態(tài)基準測試來自運行PolymorphicBenchmark ,而其他基準來自JavaFinalBenchmark
簡單的呼叫網(wǎng)站
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我們的第一組結(jié)果比較了虛擬方法, final方法和層次結(jié)構(gòu)較深且被覆蓋的方法的調(diào)用成本。 請注意,在所有這些情況下,我們都強制編譯器不內(nèi)聯(lián)方法。 正如我們所看到的,時間之間的差異很小,而且我們的平均錯誤率表明它并不重要。 因此,我們可以得出結(jié)論,僅添加final關(guān)鍵字并不會大大提高方法調(diào)用的性能。 覆蓋該方法似乎也沒有太大區(qū)別。
內(nèi)聯(lián)簡單的呼叫站點
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現(xiàn)在,我們采用了相同的三種情況,并刪除了內(nèi)聯(lián)限制。 同樣, final和virtual方法調(diào)用的結(jié)束時間彼此相似。 它們比非內(nèi)聯(lián)的情況快大約4倍,我將其歸結(jié)為內(nèi)聯(lián)本身。 在此始終被覆蓋的方法調(diào)用最終在兩者之間。 我懷疑這是因為方法本身具有多個可能的子類實現(xiàn),因此編譯器需要插入類型保護。 上面在“ 多態(tài)性”中對此機制進行了詳細說明。
類等級沖擊
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哇–這是很多方法! 每個編號的方法調(diào)用(1-4)表示調(diào)用方法的類層次結(jié)構(gòu)有多深。 所以parentMethod4意味著我們調(diào)用了在類的第4個父級上聲明的方法。 如果看一下數(shù)字,則1和4之間的差異很小。因此,我們可以得出結(jié)論,層次深度沒有區(qū)別。 可內(nèi)聯(lián)的案例都遵循相同的模式:層次深度沒有區(qū)別。 我們的inlineable方法性能與inlinableAlwaysOverriddenMethod相當,但比inlinableVirtualInvoke慢。 我再次將其歸結(jié)為所使用的類型防護。 JIT編譯器可以對方法進行概要分析,以找出僅一個內(nèi)聯(lián)方法,但是無法證明這種方法永遠存在。
類層次對
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關(guān)鍵字似乎沒有什么區(qū)別。 我會認為這是可能在這里,從理論上說,對于inlinableParentFinalMethod4來加以證明inlineable沒有型后衛(wèi),但它不會出現(xiàn)這種情況。
多態(tài)性
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最后,我們來談?wù)劧鄳B(tài)調(diào)度的情況。 單態(tài)調(diào)用費用與我們上面的常規(guī)虛擬調(diào)用費用大致相同。 由于我們需要在較大的vtable上進行查找,因此隨著雙態(tài)和多態(tài)情況的顯示,它們變得更慢。 一旦啟用內(nèi)聯(lián)類型分析,我們的單態(tài)和雙態(tài)調(diào)用站點就會降低“內(nèi)聯(lián)有警戒”方法調(diào)用的成本。 因此,與類層次結(jié)構(gòu)的情況類似,只是速度較慢。 大形情況仍然很慢。 請記住,我們這里沒有告訴熱點防止內(nèi)聯(lián),只是它沒有為比雙態(tài)更復(fù)雜的調(diào)用站點實現(xiàn)多態(tài)內(nèi)聯(lián)緩存。
我們學到了什么?
我認為值得一提的是,有很多人沒有表現(xiàn)心理模型來說明花費時間不同的不同類型的方法調(diào)用,還有很多人知道他們花費的時間不同,但實際上并沒有非常正確。 我知道我以前去過那里,做了各種各樣的錯誤假設(shè)。 因此,我希望這項調(diào)查對人們有所幫助。 這是我很樂意支持的聲明摘要。
- 最快和最慢的方法調(diào)用類型之間有很大的不同。
- 實際上,添加或刪除final關(guān)鍵字并不會真正影響性能,但是,如果您隨后重構(gòu)層次結(jié)構(gòu),事情可能會開始放慢速度。
- 更深的類層次結(jié)構(gòu)對呼叫性能沒有真正的影響。
- 單態(tài)調(diào)用比雙態(tài)調(diào)用更快。
- 雙態(tài)調(diào)用比大形調(diào)用快。
- 在可概要分析但不能證明的情況下,我們看到的類型防護在單態(tài)調(diào)用站點上確實比在可證明的單態(tài)調(diào)用站點上放慢了很多速度。
我會說類型保護程序的成本是我個人的“大啟示”。 這是我鮮為人知的話題,經(jīng)常被認為是無關(guān)緊要的。
注意事項和進一步工作
當然,這不是主題領(lǐng)域的最終決定!
- 該博客僅關(guān)注與方法調(diào)用性能有關(guān)的類型相關(guān)因素。 我沒有提到的一個因素是由于主體大小或調(diào)用堆棧深度而導(dǎo)致的圍繞方法內(nèi)聯(lián)的啟發(fā)式方法。 如果您的方法太大,則根本不會內(nèi)聯(lián),您仍然要為方法調(diào)用的費用支付費用。 編寫小的,易于閱讀的方法的另一個原因。
- 我沒有研究過接口調(diào)用如何影響這些情況。 如果您發(fā)現(xiàn)這很有趣,那么可以在Mechanical Sympathy博客上進行有關(guān)調(diào)用接口性能的調(diào)查。
- 我們在這里完全忽略的一個因素是方法內(nèi)聯(lián)對其他編譯器優(yōu)化的影響。 當編譯器執(zhí)行僅考慮一種方法的優(yōu)化(過程內(nèi)優(yōu)化)時,他們實際上希望獲得盡可能多的信息,以便有效地進行優(yōu)化。 內(nèi)聯(lián)的局限性可以大大縮小其他優(yōu)化必須使用的范圍。
- 將說明直接附加到匯編級別,以深入了解該問題。
也許這些是將來博客文章的主題。
翻譯自: https://www.javacodegeeks.com/2014/05/too-fast-too-megamorphic-what-influences-method-call-performance-in-java.html
java 調(diào)用 js性能
總結(jié)
以上是生活随笔為你收集整理的java 调用 js性能_太快了,太变态了:什么会影响Java中的方法调用性能?的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 安卓3d网游(3d安卓h游)
- 下一篇: 全民奇迹安卓苹果互通吗(全民奇迹安卓)