Apache Doris在作業(yè)幫實(shí)時(shí)數(shù)倉(cāng)中的應(yīng)用實(shí)踐
點(diǎn)擊上方藍(lán)色字體,選擇“設(shè)為星標(biāo)”

?1. 什么是空檢查
在Java里經(jīng)常會(huì)判斷一個(gè)對(duì)象是否為空,如果為空的對(duì)象訪問(wèn)方法,字段會(huì)拋出空指針異常,而空指針異常為運(yùn)行異常,如果不抓取這個(gè)異常,有的時(shí)候會(huì)導(dǎo)致程序異常,為了解決這個(gè)問(wèn)題,我們通常會(huì)在代碼里顯式的去判斷該對(duì)象是否為空,進(jìn)行為空的邏輯處理,這種做法邏輯雖然明確,但是由于空的邏輯并不是經(jīng)常碰到,這樣會(huì)導(dǎo)致有多余的邏輯分支判斷。
2. 隱式空檢查 implicit exception
我們先來(lái)看一個(gè)代碼:
public static int nullCheck(String value) {
if(value == null){
return -1;
}
else{
return value.length();
}
}
我們進(jìn)行運(yùn)行編譯獲取編譯后的匯編
0x00007f23c922f107: mov 0xc(%rsi),%eax ; implicit exception: code begin: 0x00007f23c922f107; code end: 0x00007f23c922f10a; code end: 0x00007f23c922f0e0; implicit exception: dispatches to 0x00007f23c922f1a1
0x00007f23c922f10a: push %r10
0x00007f23c922f10c: cmp 0x15deda15(%rip),%r12 # 0x00007f23df01cb28
我們并沒(méi)有看到有邏輯分支對(duì)value.length中的value進(jìn)行空指針判斷,我們?cè)谂赃叺淖⑨屩锌吹搅藰?gòu)建了Implicit Exception的跳轉(zhuǎn)地址 implicit exception: dispatches to 0x00007f23c922f1a1 mov 0xc(%rsi),%eax這個(gè)指令并不是一個(gè)跳轉(zhuǎn)指令,但為何在旁邊的代碼注釋中卻標(biāo)明了Implicit Exception呢?這是因?yàn)樵贘ava編譯的過(guò)程中會(huì)生成一段ImplicitNullCheckStub代碼,用來(lái)處理遇到Null的場(chǎng)景。
;; ImplicitNullCheckStub slow case
0x00007f23c922f1a1: callq 0x00007f23c9166460 ; OopMap{off=198}
;*invokevirtual length
; - NullCheck::hotMethod@7 (line 33)
; {runtime_call}
0x00007f23c922f1a6: mov %rsp,-0x28(%rsp)
0x00007f23c922f1ab: sub $0x80,%rsp
0x00007f23c922f1b2: mov %rax,0x78(%rsp)
0x00007f23c922f1b7: mov %rcx,0x70(%rsp)
0x00007f23c922f1bc: mov %rdx,0x68(%rsp)
0x00007f23c922f1c1: mov %rbx,0x60(%rsp)
0x00007f23c922f1c6: mov %rbp,0x50(%rsp)
0x00007f23c922f1cb: mov %rsi,0x48(%rsp)
0x00007f23c922f1d0: mov %rdi,0x40(%rsp)
0x00007f23c922f1d5: mov %r8,0x38(%rsp)
0x00007f23c922f1da: mov %r9,0x30(%rsp)
0x00007f23c922f1df: mov %r10,0x28(%rsp)
0x00007f23c922f1e4: mov %r11,0x20(%rsp)
0x00007f23c922f1e9: mov %r12,0x18(%rsp)
0x00007f23c922f1ee: mov %r13,0x10(%rsp)
0x00007f23c922f1f3: mov %r14,0x8(%rsp)
0x00007f23c922f1f8: mov %r15,(%rsp)
0x00007f23c922f1fc: movabs $0x7f23de9c944b,%rdi ; {external_word}
0x00007f23c922f206: movabs $0x7f23c922f1a6,%rsi ; {internal_word}
0x00007f23c922f210: mov %rsp,%rdx
0x00007f23c922f213: and $0xfffffffffffffff0,%rsp
0x00007f23c922f217: callq 0x00007f23de53c7a0 ; {runtime_call}
0x00007f23c922f21c: hlt
那什么時(shí)候會(huì)觸發(fā)ImplicitNullCheckStub的調(diào)用呢?因?yàn)镸ov指令當(dāng)碰到無(wú)效地址的時(shí)候,在Linux系統(tǒng)中會(huì)產(chǎn)生一個(gè)發(fā)生signalled exception(在這種情況下是SIGSEGV),這時(shí)候會(huì)轉(zhuǎn)到信號(hào)處理函數(shù),如果應(yīng)用有自定義的該信號(hào)處理函數(shù),就執(zhí)行該信號(hào)處理函數(shù)。JVM在linux下注冊(cè)了JVM_handle_linux_signal函數(shù)
else if (sig == SIGSEGV &&
!MacroAssembler::needs_explicit_null_check((intptr_t)info->si_addr)) {
// Determination of interpreter/vtable stub/compiled code null exception
stub = SharedRuntime::continuation_for_implicit_exception(thread, pc, SharedRuntime::IMPLICIT_NULL);
}
在continuation_for_implicit_exception函數(shù)里,通過(guò)當(dāng)前異常地址獲取target_pc = nm->continuation_for_implicit_exception(pc);地址,把地址內(nèi)容保存到信號(hào)處理函數(shù)的context中
if (stub != NULL) {
// save all thread context in case we need to restore it
if (thread != NULL) thread->set_saved_exception_pc(pc);
uc->uc_mcontext.gregs[REG_PC] = (greg_t)stub;
return true;
}
由linux的信號(hào)處理來(lái)跳轉(zhuǎn)到指定的stub中,也就是ImplicitNullCheckStub
在這里我們看到JVM并沒(méi)有顯示的增加指令分支對(duì)Null進(jìn)行檢查,而是通過(guò)異常信號(hào)處理機(jī)制來(lái)處理,跳轉(zhuǎn)到ImplicitNullCheckStub里單獨(dú)處理這里是有性能的損耗,為何JVM里會(huì)考慮使用異常信號(hào)處理機(jī)制,是因?yàn)榭紤]到大部分的場(chǎng)景不為空,提高執(zhí)行效率的一種方式。
3. C1的Null Eliminator
C1的Null Eliminator 于C2不太一樣, C1 的Null Eliminator 解決的是重復(fù)check null的問(wèn)題。
整體思路:
顯式的調(diào)用Nullcheck的時(shí)候,需要將顯式的NullCheck擦除,改成ImplicitNullCheck 對(duì)同一個(gè)參數(shù)使用不需要每次都引入null檢查,只要在第一次檢查后,后續(xù)就可以將null檢查給插除了。算法:數(shù)據(jù)流分析 OUT[entry] = ?; for (each basic block B\entry) { IN[B] = U P a predecessor of B OUT[P]; if (changes to IN occur)){ OUT[B] = genB U (IN[B]); } }
C1是使用SSA的表達(dá)方式,我們會(huì)發(fā)現(xiàn)沒(méi)有了傳統(tǒng)流分析算法里的Kill函數(shù),在SSA里的use-define鏈路里如果一個(gè)參數(shù)如果進(jìn)行redfine過(guò)后,參數(shù)的命名會(huì)變化,在使用的時(shí)候就已經(jīng)使用新的參數(shù)名字,這樣就天生具備了kill的能力。
我們先來(lái)看一個(gè)SSA的例子:
. 18 0 a13 null_check(a3)
. 1 0 a16 a3._12 ([) value
. 4 0 i17 a16.length
. 21 0 i19 ireturn i17
Null Eliminator 分析的是value,在上面的第一行首先現(xiàn)有Null_Check,這是在調(diào)用函數(shù)的時(shí)候,IR層添加了null_check,根據(jù)算法我們會(huì)顯示的去除null_check a3 并設(shè)置為implicit null檢查,而對(duì)第二句語(yǔ)句 a16 使用了a3 并且又跟在a13語(yǔ)句后面,故而可以直接使用第一個(gè)語(yǔ)句的implicit null check,而把第一個(gè)語(yǔ)句null check 徹底的擦除,假如后續(xù)的語(yǔ)句繼續(xù)使用a3的化,那么該語(yǔ)句的implicit null check 就可以直接擦除了。

算法其實(shí)和常見(jiàn)的流分析一樣,設(shè)置一個(gè)ValueSet,對(duì)每個(gè)參數(shù)的下標(biāo)以bit位置來(lái)保存,同時(shí)每一個(gè)Block都會(huì)保存一個(gè)ValueSet
算法實(shí)現(xiàn)細(xì)節(jié):
Null Eliminator 是一個(gè)前向分析
分析流從不同的BB塊流向的時(shí)候,每個(gè)Block都會(huì)Uion 上一個(gè)Block塊ValueSet
如果發(fā)現(xiàn)變化,就會(huì)對(duì)Block里的指令進(jìn)行遍歷分析
分析指令里的Value參數(shù)
該參數(shù)已經(jīng)在bitset里被設(shè)置過(guò),就代表已經(jīng)做過(guò)Null check
如果前面的指令做的是顯式的null check,那么插除的就是顯示的null check,補(bǔ)上Implicit null check
如果前面的指令做的是Implicit null check,那么該null check將會(huì)被Eliminator
3.1 Null Check Eliminator
void handle_AccessField (AccessField* x);
void handle_ArrayLength (ArrayLength* x);
void handle_LoadIndexed (LoadIndexed* x);
void handle_StoreIndexed (StoreIndexed* x);
void handle_NullCheck (NullCheck* x);
void handle_Invoke (Invoke* x);
void handle_NewInstance (NewInstance* x);
void handle_NewArray (NewArray* x);
void handle_AccessMonitor (AccessMonitor* x);
void handle_Intrinsic (Intrinsic* x);
void handle_ExceptionObject (ExceptionObject* x);
void handle_Phi (Phi* x);
void handle_ProfileCall (ProfileCall* x);
void handle_ProfileReturnType (ProfileReturnType* x);
在上面函數(shù)里定義的我們可以看到訪問(wèn)field, array, 顯示的null check, 調(diào)用, 初始化對(duì)象,異常對(duì)象,以及phi函數(shù) 我們?yōu)檫@里單獨(dú)的討論一下phi函數(shù):關(guān)于Phi函數(shù)是什么,在這里我們就不介紹了:先來(lái)看一段IR
B2 (V) [22, 31] pred: B10 B1
Locals:
0 a3
1 a18 [ a4 a10]
empty stack
inlining depth 0
__bci__use__tid____instr____________________________________
. 23 0 i19 a18._12 (I) x
. 27 0 a20 null_check(a3)
. 1 0 a23 a3._12 ([) value
. 4 0 i24 a23.length
30 0 i26 i19 + i24
. 31 0 i27 ireturn i26
我們可以看到a18 phi參數(shù)里面決定的是a4 a10。分析Phi函數(shù)需要分析a4, a10,如果a4, a10都已經(jīng)進(jìn)行空檢查過(guò),那么該a18也就可以進(jìn)行null Eliminator
3.2 C2 Null 優(yōu)化
C2的null優(yōu)化和C1的優(yōu)化是不一樣的,C2的Null優(yōu)化會(huì)優(yōu)化Block,通過(guò)Profile可以推斷分支是否會(huì)被執(zhí)行,如果不會(huì)被執(zhí)行,分支將會(huì)被剪支。但如果發(fā)現(xiàn)剪支錯(cuò)誤,會(huì)進(jìn)行反優(yōu)化,重新回到解釋。
但是C1是不會(huì)的,C1的優(yōu)化并不會(huì)剪支,當(dāng)程序碰到大量的Null的時(shí)候,會(huì)執(zhí)行implicit的分支,從而大大降低效率,這里需要人工的去判斷,究竟是Null多 還是非Null多,如果Null多的化,還是建議代碼里添加null 的檢查,避免效率的大大降低。

版權(quán)聲明:
文章不錯(cuò)?點(diǎn)個(gè)【在看】吧!??




