C1 RCE對%的處理

HotSpot VM的C1有個RCE(Range Check Elimination,範圍檢查消除)優化,所謂範圍檢查消除,就是為了正確的丟擲陣列越界異常,虛擬機器需要在陣列訪問的一些地方插入隱式的檢查,但是這些檢查會降低效能,比如在迴圈中每次迴圈都得檢查一次,所以HotSpot VM會想辦法在可能的地方消除這些檢查。我在看C1 RCE的時候發現目前它對求餘符號的支援較為薄弱,它只能處理形如下面的程式碼:

arr[x%arr.length] // 只有除數是x.length的時候,才能應用RCE優化

如果餘數是整數常量,它就不能工作了:

arr[x%3]
for(int i=0;i<10;i++){
arr[x%10]
}

實際上,根據JLS的定義,我們知道如果除數為整數常量(且等於零,因為0作為除數會丟擲執行時異常),是可以推匯出結果的上下界的(也取決於被除數的正負),規則如下:

x % -y ==> [0, y - 1]
x % y ==> [0, y - 1]
-x % y ==> [-y + 1, 0]
-x % -y ==> [-y + 1, 0]

於是,我給JDK發了個patch,這個問題算是解決了。但是Nils提到,C2是否有相同的優化呢?後面Tobias幫忙確認了一下C2沒有,我再後來也進一步確認了,所以下一步是調研C2是否能應用同樣的優化。

調研為C2應用同樣的優化

本來以為是比較trivial的事情,為求餘節點的型別系統加點程式碼,推導一下上下界即可,實際上我也這麼做的,但是最後發現這樣沒有消除上下界:

Node* Parse::array_addressing(BasicType type, int vals, const Type*& elemtype) {
Node *idx = peek(0+vals); // Get from stack without popping
Node *ary = peek(1+vals); // in case of exception // Null check the array base, with correct stack contents
ary = null_check(ary, T_ARRAY);
// Compile-time detect of null-exception?
if (stopped()) return top(); const TypeAryPtr* arytype = _gvn.type(ary)->is_aryptr();
const TypeInt* sizetype = arytype->size();
elemtype = arytype->elem(); if (UseUniqueSubclasses) {
...
} // Check for big class initializers with all constant offsets
// feeding into a known-size array.
const TypeInt* idxtype = _gvn.type(idx)->is_int();
// See if the highest idx value is less than the lowest array bound,
// and if the idx value cannot be negative:
bool need_range_check = true;
if (idxtype->_hi < sizetype->_lo && idxtype->_lo >= 0) {
need_range_check = false;
if (C->log() != NULL) C->log()->elem("observe that='!need_range_check'");
} ciKlass * arytype_klass = arytype->klass();
if ((arytype_klass != NULL) && (!arytype_klass->is_loaded())) {
// Only fails for some -Xcomp runs
// The class is unloaded. We have to run this bytecode in the interpreter.
uncommon_trap(Deoptimization::Reason_unloaded,
Deoptimization::Action_reinterpret,
arytype->klass(), "!loaded array");
return top();
} // Do the range check
if (GenerateRangeChecks && need_range_check) {
... // need_range_check仍然為true
}
}

need_range_check仍然為true,除錯後發現推導上下界根本沒有執行,因為C2建立完求餘節點後,會執行一個IGVN的過程,即迭代的應用多種優化,其中就包括理想化,C2理想化是指應用很多區域性小優化的過程,在這個例子中就是特殊處理形如x%2^n,x%2^n-1x%1的情況,如果除數是整數常量,它還會使用一個來自https://book.douban.com/subject/1784887/書裡面的演算法,即Division by Invariant Integers using Multiplication(by Granlund and Montgomery),搜了一下知乎有類似的文章,想要了解細節可以讀讀https://zhuanlan.zhihu.com/p/151038723。知道了原因,於是我改了下程式碼,禁止了求餘節點的理想化,心想這總可以了吧。

還是不行

儘管我已經禁止了對求餘符號的理想化優化,但是範圍檢查還是生成了。。。我又繼續看程式碼,發現除了理想化的這個優化之外,C2在IR(中間表示)構造的過程中又 又 又 又 又對求餘運算做了個優化!如果除數是正整數常量,且是2^n,那麼C2會對它進行變形,IR如圖所示:

左邊的IR是 IR構造的時候C2做的優化後的效果,右邊是理想化優化後的效果。實際上它們做的事情本身是比較重複的,而且經過測試發現,理想化優化的演算法要好於IR構造過程中的優化,所以我又提了個patch,嘗試解決這個問題(不過還在review中)。

結語

我認為為求餘節點推導上下界也是有意義的,如果以後有其他優化會變形為求餘運算,那麼它們可以應用這個推導,同時,為求餘做統一完善的型別推導這件事本身也是正確的,所以我又提了個patch。儘管如此,可以看到最終我只消除了C1 arr[x%4]的範圍檢查,還是沒能消除C2 arr[x%4]的範圍檢查,是不是以後可以說C1有的地方做的比C2好了(狗頭hh。