【譯】JVM Anatomy Park #16: 超多態(tài)虛調(diào)用

原文地址:JVM Anatomy Park #16: Megamorphic Virtual Calls

問(wèn)題

我聽說(shuō)超多態(tài)虛調(diào)用(megamorphic virtual calls)非常糟糕,因?yàn)檫@種調(diào)用是由解釋器執(zhí)行的绿贞,而不是優(yōu)化編譯器调塌。這是真的么?

理論

如果你讀過(guò)許多 Hotspot 中關(guān)于虛調(diào)用優(yōu)化的文章惩阶,你可能會(huì)有這樣的印象:超多態(tài)調(diào)用邪惡到家了匈辱,因?yàn)樗鼈儓?zhí)行慢路徑處理寺擂,無(wú)法獲得編譯器優(yōu)化的好處。如果你嘗試?yán)斫庹{(diào)用去虛化失敗之后 OpenJDK 的行為砾医,那么你可能會(huì)驚訝這導(dǎo)致的性能問(wèn)題拿撩。但是要考慮到,即使是基準(zhǔn)編譯器如蚜,JVM 也工作地相當(dāng)好压恒,在某些情況下,即使是解釋器的性能也是可以接受的(并且這關(guān)系到 time-to-performance)错邦。

所以探赫,現(xiàn)在下結(jié)論說(shuō)運(yùn)行時(shí)系統(tǒng)只是放棄優(yōu)化還為時(shí)過(guò)早?

實(shí)踐

讓我們嘗試看看虛調(diào)用慢路徑撬呢。因此我們?cè)?JMH 測(cè)試用例中制造了人為的超多態(tài)調(diào)用點(diǎn):使三個(gè)子類訪問(wèn)同一個(gè)調(diào)用點(diǎn):

import org.openjdk.jmh.annotations.*;

@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@Fork(1)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@State(Scope.Benchmark)
public class VirtualCall {

    static abstract class A {
        int c1, c2, c3;
        public abstract void m();
    }

    static class C1 extends A {
        public void m() { c1++; }
    }
    static class C2 extends A {
        public void m() { c2++; }
    }
    static class C3 extends A {
        public void m() { c3++; }
    }

    A[] as;

    @Param({"mono", "mega"})
    private String mode;

    @Setup
    public void setup() {
        as = new A[300];
        boolean mega = mode.equals("mega");
        for (int c = 0; c < 300; c += 3) {
            as[c]   = new C1();
            as[c+1] = mega ? new C2() : new C1();
            as[c+2] = mega ? new C3() : new C1();
        }
    }

    @Benchmark
    public void test() {
        for (A a : as) {
            a.m();
        }
    }
}

為了簡(jiǎn)化分析伦吠,我們?cè)O(shè)置參數(shù) -XX:LoopUnrollLimit=1 -XX:-TieredCompilation:禁止循環(huán)展開,以免反匯編代碼過(guò)于復(fù)雜,關(guān)閉分層編譯毛仪,保證只用最終優(yōu)化編譯器搁嗓。雖然我們不太關(guān)心性能數(shù)值,但是讓我們用這些數(shù)據(jù)構(gòu)建分析框架:

Benchmark         (mode)  Mode  Cnt     Score    Error  Units
VirtualCall.test    mono  avgt    5   325.478 ± 18.156  ns/op
VirtualCall.test    mega  avgt    5  1070.304 ± 53.910  ns/op

為了了解使用優(yōu)化編譯器的情況箱靴,設(shè)置參數(shù) -XX:CompileCommand=exclude,org.openjdk.VirtualCall::test

Benchmark         (mode)  Mode  Cnt      Score     Error  Units
VirtualCall.test    mono  avgt    5  11598.390 ± 535.593  ns/op
VirtualCall.test    mega  avgt    5  11787.686 ± 884.384  ns/op

所以谱姓,超多態(tài)調(diào)用確實(shí)很低效,但是這絕不是解釋器的問(wèn)題刨晴。在優(yōu)化過(guò)的情況下 “mono” 與 “mega” 的差別基本上是調(diào)用開銷:在 “mega” 情況下每個(gè)元素耗費(fèi) 3ns,然而在 “mono” 情況下每個(gè)元素僅僅耗費(fèi) 1ns路翻。

通過(guò) perfasm 輸出 “mega” 情況下的執(zhí)行情況狈癞。為了看得清晰,這里刪除了一些內(nèi)容:

....[Hottest Region 1].......................................................................
C2, org.openjdk.generated.VirtualCall_test_jmhTest::test_avgt_jmhStub, version 88 (143 bytes)

  6.93%    5.40%  ↗  0x...5c450: mov    0x40(%rsp),%r9
                  │  ...
  3.65%    4.31%  │  0x...5c47b: callq  0x...0bf60 ;*invokevirtual m
                  │                            ; - org.openjdk.VirtualCall::test@22 (line 76)
                  │                            ;   {virtual_call}
  3.12%    2.34%  │  0x...5c480: inc    %ebp
  3.33%    0.02%  │  0x...5c482: cmp    0x10(%rsp),%ebp
                  ╰  0x...5c486: jl     0x...5c450
                     ...
.............................................................................................
 31.26%   21.77%  <total for region 1>

....[Hottest Region 2].......................................................................
C2, org.openjdk.VirtualCall$C1::m, version 84 (14 bytes) <--- mis-attributed :(

                     ...
                   Decoding VtableStub vtbl[5]@12
  3.95%    1.57%     0x...59bf0: mov    0x8(%rsi),%eax
  3.73%    3.34%     0x...59bf3: shl    $0x3,%rax
  3.73%    5.04%     0x...59bf7: mov    0x1d0(%rax),%rbx
 16.45%   22.42%     0x...59bfe: jmpq   *0x40(%rbx)        ; jump to target
                     0x...59c01: add    %al,(%rax)
                     0x...59c03: add    %al,(%rax)
                     ...
.............................................................................................
 27.87%   32.37%  <total for region 2>

....[Hottest Region 3].......................................................................
C2, org.openjdk.VirtualCall$C3::m, version 86 (26 bytes)

# {method} {0x00007f75aaf4dd50} 'm' '()V' in 'org/openjdk/VirtualCall$C3'

                    ...
                  [Verified Entry Point]
 17.82%   26.04%    0x...595c0: sub    $0x18,%rsp
  0.06%    0.04%    0x...595c7: mov    %rbp,0x10(%rsp)
                    0x...595cc: incl   0x14(%rsi)       ; c3++
  3.53%    5.14%    0x...595cf: add    $0x10,%rsp
                    0x...595d3: pop    %rbp
  3.29%    5.10%    0x...595d4: test   %eax,0x9f01a26(%rip)
  0.02%    0.02%    0x...595da: retq
                    ...
.............................................................................................
 24.73%   36.35%  <total for region 3>

所以性能測(cè)試調(diào)用了一些東西茂契,我們可以假設(shè)為虛調(diào)用處理程序蝶桶,然后以 VirtualStub 結(jié)束,這應(yīng)該是所有運(yùn)行時(shí)對(duì)虛調(diào)用所做的事情:在虛方法表(VMT)的幫助下跳轉(zhuǎn)到實(shí)際的方法掉冶。[1]

但是等一下真竖,這里不是這樣!反匯編代碼顯示實(shí)際調(diào)用的是 0x…?0bf60厌小,而不是在 0x…?59bf0VirtualStub恢共?!并且這個(gè)調(diào)用很頻繁璧亚,所以調(diào)用的目標(biāo)也應(yīng)該很頻繁讨韭,對(duì)么?這就是運(yùn)行時(shí)系統(tǒng)戲弄我們的地方癣蟋。即使編譯器放棄優(yōu)化虛調(diào)用透硝,運(yùn)行時(shí)也可以自行處理“悲觀”的情況。為了更好的診斷問(wèn)題疯搅,我們需要獲取 fastdebug OpenJDK 構(gòu)建濒生,這提供了內(nèi)聯(lián)緩存(IC) 的追蹤選項(xiàng):-XX:+TraceIC。另外幔欧,我們通過(guò) -prof perfasm:saveLog=true 保存 Hotspot 日志

你瞧罪治!

$ grep IC org.openjdk.VirtualCall.test-AverageTime.log
    IC@0x00007fac4fcb428b: to megamorphic {method} {0x00007fabefa81880} 'm' ()V';
                                 in 'org/openjdk/VirtualCall$C2'; entry: 0x00007fac4fcb2ab0

好的,內(nèi)聯(lián)緩存代替了位于0x00007fac4fcb428b 的調(diào)用點(diǎn)礁蔗。這是什么规阀?這是我們的 Java 調(diào)用!

$ grep -A 4 0x00007fac4fcb428b: org.openjdk.VirtualCall.test-AverageTime.log
   0.02%    0x00007fac4fcb428b: callq  0x00007fac4fb7dda0
                                  ;*invokevirtual m {reexecute=0 rethrow=0 return_oop=0}
                                  ; - org.openjdk.VirtualCall::test@22 (line 76)
                                  ;   {virtual_call}

但是這個(gè) Java 調(diào)用中的地址是什么瘦麸?解析運(yùn)行時(shí)存根:

$ grep -C 2  0x00007fac4fb7dda0 org.openjdk.VirtualCall.test-AverageTime.log
                    0x00007fac4fb7dcdf: hlt
                  Decoding RuntimeStub - resolve_virtual_call 0x00007fac4fb7dd10
                    0x00007fac4fb7dda0: push   %rbp
                    0x00007fac4fb7dda1: mov    %rsp,%rbp
                    0x00007fac4fb7dda4: pushfq

這基本上是由運(yùn)行時(shí)調(diào)用的谁撼,找出我們想要調(diào)用的方法,然后讓 IC 修補(bǔ)指向新解析地址的調(diào)用!因?yàn)檫@是一次性操作厉碟,難怪不會(huì)出現(xiàn)在熱代碼中喊巍。IC 操作行提示修改入口為另一個(gè)地址,順便說(shuō)一下箍鼓,也就是實(shí)際的 VtableStub:

$ grep -C 4 0x00007fac4fcb2ab0: org.openjdk.VirtualCall.test-AverageTime.log
                  Decoding VtableStub vtbl[5]@12
  8.94%    6.49%    0x00007fac4fcb2ab0: mov    0x8(%rsi),%eax
  0.16%    0.06%    0x00007fac4fcb2ab3: shl    $0x3,%rax
  0.20%    0.10%    0x00007fac4fcb2ab7: mov    0x1e0(%rax),%rbx
  2.34%    1.90%    0x00007fac4fcb2abe: jmpq   *0x40(%rbx)
                    0x00007fac4fcb2ac1: int3

最后就不需要通過(guò)運(yùn)行時(shí)和編譯器調(diào)用解析邏輯來(lái)分發(fā)了崭参,解析邏輯就是調(diào)用做 VMT 分發(fā)的 VtableStub —— 從不離開生成的機(jī)器碼。IC 機(jī)制將會(huì)以相同的方式處理虛單態(tài)和靜態(tài)調(diào)用款咖,指向不需要做 VMT 分發(fā)的存根和地址何暮。

我們從第一次 JMH perfasm 輸出中看到的像是編譯之后,但是執(zhí)行和運(yùn)行時(shí)優(yōu)化之前的代碼铐殃。[2]

觀察

僅僅因?yàn)榫幾g器未能優(yōu)化到最佳情況海洼,并不意味著最壞情況會(huì)更糟糕。誠(chéng)然富腊,你會(huì)放棄一些優(yōu)化坏逢,但是成本不足以大到完全避免虛調(diào)用。這個(gè)觀點(diǎn)與“Java 方法分發(fā)的黑魔法” 的結(jié)論一致:除非你非常關(guān)心赘被,否則沒有必要擔(dān)心調(diào)用的性能是整。


[1] 接口調(diào)用的處理方式與此類似,但是在存根中解析和調(diào)用的過(guò)程會(huì)有一些變化民假。

[2] “分析器是說(shuō)謊的霍比特人 (并且我們討厭它們8∪搿)” 的另一個(gè)例子

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市羊异,隨后出現(xiàn)的幾起案子舵盈,更是在濱河造成了極大的恐慌,老刑警劉巖球化,帶你破解...
    沈念sama閱讀 211,194評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件秽晚,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡筒愚,警方通過(guò)查閱死者的電腦和手機(jī)赴蝇,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,058評(píng)論 2 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)巢掺,“玉大人句伶,你說(shuō)我怎么就攤上這事÷降恚” “怎么了考余?”我有些...
    開封第一講書人閱讀 156,780評(píng)論 0 346
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)轧苫。 經(jīng)常有香客問(wèn)我楚堤,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,388評(píng)論 1 283
  • 正文 為了忘掉前任身冬,我火速辦了婚禮衅胀,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘酥筝。我一直安慰自己滚躯,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,430評(píng)論 5 384
  • 文/花漫 我一把揭開白布嘿歌。 她就那樣靜靜地躺著掸掏,像睡著了一般。 火紅的嫁衣襯著肌膚如雪宙帝。 梳的紋絲不亂的頭發(fā)上丧凤,一...
    開封第一講書人閱讀 49,764評(píng)論 1 290
  • 那天,我揣著相機(jī)與錄音茄唐,去河邊找鬼。 笑死蝇更,一個(gè)胖子當(dāng)著我的面吹牛沪编,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播年扩,決...
    沈念sama閱讀 38,907評(píng)論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼蚁廓,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了厨幻?” 一聲冷哼從身側(cè)響起相嵌,我...
    開封第一講書人閱讀 37,679評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤溃论,失蹤者是張志新(化名)和其女友劉穎费薄,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體凶异,經(jīng)...
    沈念sama閱讀 44,122評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡格了,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,459評(píng)論 2 325
  • 正文 我和宋清朗相戀三年看铆,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片盛末。...
    茶點(diǎn)故事閱讀 38,605評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡弹惦,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出悄但,到底是詐尸還是另有隱情棠隐,我是刑警寧澤,帶...
    沈念sama閱讀 34,270評(píng)論 4 329
  • 正文 年R本政府宣布檐嚣,位于F島的核電站助泽,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜报咳,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,867評(píng)論 3 312
  • 文/蒙蒙 一侠讯、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧暑刃,春花似錦厢漩、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,734評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至架谎,卻和暖如春炸宵,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背谷扣。 一陣腳步聲響...
    開封第一講書人閱讀 31,961評(píng)論 1 265
  • 我被黑心中介騙來(lái)泰國(guó)打工土全, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人会涎。 一個(gè)月前我還...
    沈念sama閱讀 46,297評(píng)論 2 360
  • 正文 我出身青樓裹匙,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親末秃。 傳聞我的和親對(duì)象是個(gè)殘疾皇子概页,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,472評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn)练慕,斷路器惰匙,智...
    卡卡羅2017閱讀 134,629評(píng)論 18 139
  • 這篇文章是我之前翻閱了不少的書籍以及從網(wǎng)絡(luò)上收集的一些資料的整理,因此不免有一些不準(zhǔn)確的地方铃将,同時(shí)不同JDK版本的...
    高廣超閱讀 15,564評(píng)論 3 83
  • 早晨被鬧鈴聲驚醒项鬼,揉揉發(fā)脹的眼睛,望見窗簾縫隙外漸漸發(fā)白的天空劲阎,新的一天開始了秃臣。 這是我元旦假期的第二天,和往常一...
    斯文韞閱讀 879評(píng)論 0 7
  • 我雙手合十 世界便在我千里之外 擁擠的人群 客套是種傷害 那許多無(wú)法深入的情感 逐一凋零 還有那些愛與憎 夢(mèng)與幻等...
    妖精的瓶子閱讀 312評(píng)論 0 6
  • 今天哪工,好久不見的老友奥此,相約參加一場(chǎng)書會(huì)分享“草木有本心”。東南大學(xué)人文學(xué)院張娟教授雁比,從一年四季稚虎,每個(gè)節(jié)氣的不同植物...
    七點(diǎn)起床閱讀 685評(píng)論 0 2