「譯」Graal JIT編譯器是如何工作的
阿新 • • 發佈:2020-05-04
原文[Understanding How Graal Works - a Java JIT Compiler Written in Java](https://chrisseaton.com/truffleruby/jokerconf17/),講了jvmci和ideal graph的基本概念以及一些優化技術,很不錯的一篇文章,開頭結尾不太重要的部分已經省略,請見諒。
## JIT編譯器是什麼
我敢說很多讀者都知道JIT編譯器是什麼,但是我還是會覆蓋基本概念,讓在場各位都沒有基礎上的疑問。
當你執行javac命令,或者用IDE儲存的時候就做編譯,你的java程式會從java程式碼編譯成JVM位元組碼。JVM位元組碼是Java程式碼的二進位制形式的表示。它比起原始碼來說更緊湊,更簡單。但是絕大多數CPU是不能直接執行JVM位元組碼的。
要執行java程式需要jvm解釋位元組碼。解釋位元組碼通常都會比執行在真正CPU上的機器程式碼要慢,所以JVM可以在執行時使用其他編譯器,將位元組碼編譯成你的CPU可以直接跑的機器程式碼。
這個編譯器通常會比javac更復雜,會做很多優化來產出高質量的機器程式碼。
## 為什麼JIT編譯器要用Java來寫
OpenJDK是JVM的一個實現,現在它包含兩個JIT編譯器。客戶端編譯器,也叫C1,設計目的是編譯速度要快,所以產出的程式碼會包含較少優化。伺服器編譯器,也叫opto或者C2,設計初衷是花更多的時間產出高效優化的程式碼。
兩者的設計動機是C1適合桌面應用程式,它們不能容忍因為JIT編譯導致長時間的停頓,C2適合長時間執行的伺服器程式,它們可以花更多時間在編譯上面。
現在的JVM將它們組合在了一起,程式碼首先使用C1編譯,如果後面還在執行而且值得為它花費額外時間就使用C2再做編譯。這個機制叫做分層編譯。
讓我們把目光轉向C2——花更多時間在優化上面
我們可以從github克隆openjdk映象,或者可以直接瀏覽它的網站下載。
```bash
$ git clone https://github.com/dmlloyd/openjdk.git
```
C2程式碼位於openjdk/hotspot/src/share/vm/opto.
![](https://img2020.cnblogs.com/blog/1654682/202005/1654682-20200504113852449-337749058.jpg)
首先,C2使用C++寫的。當然C++沒什麼本質上的錯誤,但卻有一些麻煩。C++是一門不安全的語言——這意味這C++的錯誤可以造成虛擬機器crash。也由於程式碼年代久遠,用C++寫的C2變得很難維護,很難擴充套件。
C2背後的關鍵人物Cliff Click說他再也不會用C++寫虛擬機器,我們也聽Twitter JVM團隊說C2目前是一灘死水,亟待一個替代品,因為在它上面開發太困難了。
![](https://img2020.cnblogs.com/blog/1654682/202005/1654682-20200504113956009-1045189515.jpg)
![](https://img2020.cnblogs.com/blog/1654682/202005/1654682-20200504114001673-376349923.jpg)
所以回到問題,為什麼Java可以幫我們解決這些問題?呃因為上述所有需求都暗示我們用Java而不是C++。Java異常比C++ cash更安全,沒有記憶體洩漏,沒有懸空指標,也有很多工具比如偵錯程式,profiler,visualvm,還有很多ide支援,等等。
你可能會想用Java寫一個JIT編譯器這怎麼可能?你可能認為只有使用低階系統語言比如C++才能做到,但是在這個talk中我想說不是的,完全不是!事實上JIT編譯器要做的只是接受JVM位元組碼然後產出機器程式碼——你收到一個byte[]陣列然後返還一個byte[]即可。它可能背後做很多複雜的工作,但是這完全不涉及一個真的“系統”,你也不需要一個“系統”語言——一些定義認為系統語言是指類似C或者C++的語言,但不是Java。
## 配置graal
我們要做的第一件事是java9。graal使用的介面叫做jvmci,由JEP 243Java-Level JVM Compiler Interface提案加入到Java中。 第一個實現提案的版本是java9。我使用9+181。如果有特殊需求也可以使用backport jvmci的java8。
```bash
$ export JAVA_HOME=`pwd`/jdk9
$ export PATH=$JAVA_HOME/bin:$PATH
$ java -version
java version "9"
Java(TM) SE Runtime Environment (build 9+181)
Java HotSpot(TM) 64-Bit Server VM (build 9+181, mixed mode)
```
下一步需要一個構建工具mx。 他有點像maven或gradle,但是可能你從沒在你的程式上用過它。它支援一些複雜的使用樣例,但是我們只使用它做簡單的構建工作。
```bash
$ git clone https://github.com/graalvm/mx.git
$ cd mx; git checkout 7353064
$ export PATH=`pwd`/mx:$PATH
```
接著我們克隆graal本身。。我是用graalvm的一個版本,版本號是0.28.2
```bash
$ git clone https://github.com/graalvm/graal.git --branch vm-enterprise-0.28.2
```
該倉庫包含了一些專案,目前我們不關心。我們可以切換到compiler子目錄,那就是graal jit本身。然後使用mx構建它。
```bash
$ cd graal/compiler
$ mx build
```
現在我要使用eclipse ide開啟graal原始碼。我是用eclipse 4.7.1。 mx支援生成eclipse專案檔案。
```bash
$ mx eclipseinit
```
如果你想使用graal作為workspace,點File,Import...,General,Existing projects然後選擇graal目錄即可。如果你沒用Java9執行eclipse本身,那你可能需要attach一個jdk。
![](https://img2020.cnblogs.com/blog/1654682/202005/1654682-20200504114223589-1668043741.jpg)
ok,現在萬事俱備,以一個簡單的代為例我會展示它是如何工作的。
```java
class Demo {
public static void main(String[] args) {
while (true) {
workload(14, 2);
}
}
private static int workload(int a, int b) {
return a + b;
}
}
```
我們將用javac編譯,然後使用jvm執行。首先使用傳統的C2 JIT。在這之前我們加上一些引數,用-XX:+PrintCompilation jvm記錄哪些方法便已過,用-XX:CompileOnly=Demo::workload讓編譯器只編譯某個方法,免得輸出太多了。
```bash
$ javac Demo.java
$ java \
-XX:+PrintCompilation \
-XX:CompileOnly=Demo::workload \
Demo
...
113 1 3 Demo::workload (4 bytes)
...
```
上面的log表示workload方法被編譯了,其他細節資訊我不做解釋。
現在讓我們使用剛剛構建的Graal作為Java9 JVM的JIT編譯器。我們需要再加一些比較複雜的flags。
·--module-path=...和--upgrade-module-path=...把graal加入模組路徑。注意模組是Jigsaw模組系統的東西,現在已經加入Java9,目前來說你可以將模組路徑看成是classpath。
我們需要-XX:+UnlockExperimentalVMOptions因為JVMCI(graal使用的)現目前還是實驗性質的。
然後加上-XX:+EnableJVMCI告訴JVM我們要使用JVMCI,然後加上-XX:+UseJVMCICompiler告訴jvm我們想配置一個新的JIT編譯器。
接著簡單起見,加上-XX:-TieredCompilation關閉分層編譯讓JVM只有一個JVMCI編譯器而不是C1和JVMCI混合分層。
當然,前面的-XX:+PrintCompilation 和-XX:CompileOnly=Demo::workload還是保持不變。和之前一樣,我們會看到有一個方法被編譯了雖然是使用graal編譯的。現在請只管跟著我做。
```bash
$ java \
--module-path=graal/sdk/mxbuild/modules/org.graalvm.graal_sdk.jar:graal/truffle/mxbuild/modules/com.oracle.truffle.truffle_api.jar \
--upgrade-module-path=graal/compiler/mxbuild/modules/jdk.internal.vm.compiler.jar \
-XX:+UnlockExperimentalVMOptions \
-XX:+EnableJVMCI \
-XX:+UseJVMCICompiler \
-XX:-TieredCompilation \
-XX:+PrintCompilation \
-XX:CompileOnly=Demo::workload \
Demo
...
583 25 Demo::workload (4 bytes)
...
```
## JVMCI編譯器介面
我們現在做的很牛逼了不是嗎?我們有個JVM,然後用新的JIT替換了之前的那個,還用它編譯了程式碼,且沒有改變JVM的任何程式碼。讓這一切變得可能的正是JVMCI——JVM編譯器介面——之前提到過JEP243提出它現在已經併入java9。
這個idea和jvm現有的一些技術其實差不多。
以前你可能用Java註解處理API新增過一些註解到javac,它們可以處理原始碼。不難看出java註解就是個獲取它們附著於的原始碼,然後產出新原始碼的工具。
你可能使用過java agent做自定義的java位元組碼處理。它可以攔截java位元組碼然後修改它。
JVMCI和它們一樣。它讓你可以插入一個java寫的jit編譯器到jvm上。
等下我會介紹ppt上展示的程式碼的一些方法,然後我會簡化識別符號和邏輯幫助你理解這個idea,接著我會切換到eclpse的一些螢幕截圖向你展示真的程式碼,雖然可能有點複雜,但是大體上是一樣的。這個talk的重要內容就是幫助你深入原始碼本身,所以我不想隱藏它,儘管它很複雜。
首先我想消除你覺得jit編譯器極其複雜的想法。JIT編譯器的輸入什麼?它獲取要編譯的方法的位元組碼,位元組碼,看名字就知道是“一個位元組陣列的程式碼”。JIT編譯器輸出什麼?它輸出方法對應的機器程式碼。機器程式碼也是“一個位元組陣列的程式碼“
所以當你寫一個新jit插入到jvm的時候你要實現的介面看起來類似下面:
```java
interface JVMCICompiler {
byte[] compileMethod(byte[] bytecode);
}
```
所以你大可不必認為java怎麼能做jit編譯產出機器碼這麼底層的事情,他就是個byte[]到byte[]的函式而已。
不過實際上的比這要複雜一些。只是一個位元組陣列的程式碼還不夠,我們還想要一些資訊比如區域性變數的個數,棧的大小,直譯器收集到的profiling資訊等,這讓我們可以瞭解實際程式碼執行的情況。因此輸入不是位元組陣列而是一個CompilationRequest,它可以告訴我們哪一個JavaMethod要編譯,然後給我們需要的所有資訊。
```java
interface JVMCICompiler {
void compileMethod(CompilationRequest request);
}
interface CompilationRequest {
JavaMethod getMethod();
}
interface JavaMethod {
byte[] getCode();
int getMaxLocals();
int getMaxStackSize();
ProfilingInfo getProfilingInfo();
...
}
```
同樣,介面不返回編譯好的機器程式碼。取而代之是你呼叫其他API告訴虛擬機器你想裝配上機器程式碼。
```java
HotSpot.installCode(targetCode);
```
現在為jvm寫一個jit編譯器我們只需要實現這個介面就好了。我們得到了想要編譯的方法的資訊,然後編譯成機器程式碼,最後呼叫installCode
```java
class GraalCompiler implements JVMCICompiler {
void compileMethod(CompilationRequest request) {
HotSpot.installCode(...);
}
}
```
讓我們切換到eclipse ide,看看這些介面到底長什麼樣。和我前面說的一樣,他有點複雜但不是很複雜
![](https://img2020.cnblogs.com/blog/1654682/202005/1654682-20200504114441756-148727540.jpg)
![](https://img2020.cnblogs.com/blog/1654682/202005/1654682-20200504114449816-139081777.jpg)
![](https://img2020.cnblogs.com/blog/1654682/202005/1654682-20200504114454908-644474988.jpg)
現在我想像你展示的就是我們可以馬上改graal的程式碼然後在java9中使用它。我會在graal編譯方法的地方加一些log程式碼
```java
class HotSpotGraalCompiler implements JVMCICompiler {
CompilationRequestResult compileMethod(CompilationRequest request) {
System.err.println("Going to compile " + request.getMethod().getName());
...
}
}
```
![](https://img2020.cnblogs.com/blog/1654682/202005/1654682-20200504114526226-953066452.jpg)
現在關閉HotSpot的編譯記錄,然後用我們修改的版本
```bash
$ java \
--module-path=graal/sdk/mxbuild/modules/org.graalvm.graal_sdk.jar:graal/truffle/mxbuild/modules/com.oracle.truffle.truffle_api.jar \
--upgrade-module-path=graal/compiler/mxbuild/modules/jdk.internal.vm.compiler.jar \
-XX:+UnlockExperimentalVMOptions \
-XX:+EnableJVMCI \
-XX:+UseJVMCICompiler \
-XX:-TieredCompilation \
-XX:CompileOnly=Demo::workload \
Demo
```
如果你在elipse中編輯,你會注意到我們甚至沒有執行mx build。正常的eclipse編譯即可。我們完全不需要編譯jvm本身。我們可以看可以立刻把修改好的編譯器插入到現有的jvm。
## Graal graph
ok,現在我們知道graal是把byte[]轉成另一個byte[]。讓我們說說這個轉化背後 的理論和資料結構。因為它有點不尋常,即使是對於編譯器來說。
本質來說編譯器做的事情是操縱程式。要操縱程式需要一些用來表示程式本身的資料結構。位元組碼和類似的指令序列都行,但是表達力不強。
相反,graal使用圖結構來表示你的程式。如果你對兩個區域性變數做加飯,圖將為每個區域性變數建立一個node,還有一個表示加法的node,兩條邊指示區域性變數node流入加法node。
這種圖也被叫做程式依賴圖。
如果有一個表示式比如x+y,我們會看到兩個表示x,y的node,一個對兩者做加法的node。
![](https://img2020.cnblogs.com/blog/1654682/202005/1654682-20200504114619639-1496047935.jpg)
圖中藍色的邊表示資料流,它們讀取區域性變數的值,流入加法node。我們也可以使用邊表示程式執行的順序。如果我們呼叫兩個方法而不是讀取兩個區域性變數,比如getX(),getY(),然後我們需要記住呼叫順序。這個也可以通過一條邊來表示,即下面的紅邊。
![](https://img2020.cnblogs.com/blog/1654682/202005/1654682-20200504114635706-1787437913.jpg)
graal圖可以說是將兩種圖以某種方式結合到了一起。節點相同,但是一個邊集(edge set)表示資料流,另一個邊集表示控制流。
你可以用IdealGraphVisualiser這個工具視覺化graal圖。執行mx igv即可。
![](https://img2020.cnblogs.com/blog/1654682/202005/1654682-20200504114656911-1862361781.jpg)
然後jvm加上引數-Dgraal.Dump
我們可以寫個簡單的表示式看看資料流
```java
int average(int a, int b) {
return (a + b) / 2;
}
```
![](https://img2020.cnblogs.com/blog/1654682/202005/1654682-20200504114725699-1903361085.jpg)
你可以看到引數0(寫作P(0))和引數1(寫作P(1))是如何送入加法操作的,然後結果是如何和常量2(寫作C(2))送入除法操作的。計算最後結果返回。
如果我們引入一個迴圈可以看到更復雜的資料流和控制流
```java
int average(int[] values) {
int sum = 0;
for (int n = 0; n < values.length; n++) {
sum += values[n];
}
return sum / values.length;
}
```
![](https://img2020.cnblogs.com/blog/1654682/202005/1654682-20200504114756710-1758937267.jpg)
![](https://img2020.cnblogs.com/blog/1654682/202005/1654682-20200504114814075-109456784.jpg)
現在圖中有節點表示迴圈起始,有節點表讀取陣列元素,有節點讀取陣列長度。和之前一樣,藍線表示資料流,紅線表示控制流。
從上面不難看出為什麼這個資料結構有時候又叫做節點海(sea of nodes),或者節點湯(soup of nodes)。
我想說c2使用非常類似的資料結構,也正是因為c2使得這種節點海編譯器變得留下,它不是graal的創新。
關於圖是怎麼構建的這裡我先不展示,當你的程式使用graal graph這種格式,編譯和優化都修改這個資料結構而已。這也是為什麼用java寫jit沒問題的原因。java是oop語言,graal graph是物件的集合,然後引用視作邊將他們連結起來。
## 從位元組碼到機器程式碼
讓我們從實際除法,看看編譯的流程。
### 1.位元組碼輸入
編譯始於位元組碼。我們回到之前的簡單加法示例。
```java
int workload(int a, int b) {
return a + b;
}
```
我們在編譯開始位置輸出要編譯的位元組碼
```java
class HotSpotGraalCompiler implements JVMCICompiler {
CompilationRequestResult compileMethod(CompilationRequest request) {
System.err.println(request.getMethod().getName() + " bytecode: "
+ Arrays.toString(request.getMethod().getCode()));
...
}
}
```
```java
workload bytecode: [26, 27, 96, -84]
```
### 2.位元組碼解析器和圖構造器
位元組陣列被解析為jvm位元組碼然後放入圖構造器。這是另一種形式的直譯器,只是比較抽象。它解釋執行位元組碼,只是不傳遞它們的值而是傳遞邊然後將他們連線起來。讓我們享受一下java寫graal的好處,用elipse navigation工具看看它怎麼工作的。我們知道示例有一個加法節點,現在看看它們是怎麼建立的。
![](https://img2020.cnblogs.com/blog/1654682/202005/1654682-20200504115010218-1167078606.jpg)
![](https://img2020.cnblogs.com/blog/1654682/202005/1654682-20200504115015866-574668166.jpg)
![](https://img2020.cnblogs.com/blog/1654682/202005/1654682-20200504115023231-1400589940.jpg)
可以看到位元組碼解析器解析到iadd的時候建立了它們。如果這是一個真的jvm直譯器,他會pop兩個棧上的值,然後做加法,然後push結果到棧上。現在從棧上pop兩個node表示計算的運算元,然後新增一個node表示加法操作,最後把它們push到棧上表示計算結果。如此操作我們得倒了graal graph。
### 3. 生成彙編
我們想要將graal graph轉化為機器程式碼,需要為圖中每個node生成機器程式碼。這一步在generate方法中完成。
```java
void generate(Generator gen) {
gen.emitAdd(a, b);
}
```
再一次的,我們在高層次抽象上面工作,又一個類生成類加法的彙編,但是不知道細節。emitAdd的細節有點複雜和抽象,因為算數操作會根據operand的不同很多組合,並且不同的操作符可以共享絕大部分程式碼,所以我們簡化一下程式
```java
int workload(int a) {
return a + 1;
}
```
它使用加法指令,然後我會向你展示類似的彙編
```java
void incl(Register dst) {
int encode = prefixAndEncode(dst.encoding);
emitByte(0xFF);
emitByte(0xC0 | encode);
}
void emitByte(int b) {
data.put((byte) (b & 0xFF));
}
```
![](https://img2020.cnblogs.com/blog/1654682/202005/1654682-20200504115140000-1645047889.jpg)
![](https://img2020.cnblogs.com/blog/1654682/202005/1654682-20200504115147094-477005473.jpg)
可以看到它生成位元組作為輸出,這些都放到一個標準的ByteBuffer裡面——它可以用來構建位元組陣列
### 4. 彙編輸出
和前面提到的位元組碼輸入一樣,我們看看機器程式碼的輸出是什麼樣的。我們改一下原始碼讓他log生成的機器碼
```java
class HotSpotGraalCompiler implements JVMCICompiler {
CompilationResult compileHelper(...) {
...
System.err.println(method.getName() + " machine code: "
+ Arrays.toString(result.getTargetCode()));
...
}
}
```
![](https://img2020.cnblogs.com/blog/1654682/202005/1654682-20200504115224093-2125448416.jpg)
我們也可以使用一個工具反彙編生成的呃機器程式碼,它是hotspot的標準組件——不是graal的。我會展示怎麼構建這個工具——它在openjdk倉庫中但是預設沒有在jvm中,所以只得自己構建它
```bash
$ cd openjdk/hotspot/src/share/tools/hsdis
$ curl -O http://ftp.heanet.ie/mirrors/gnu/binutils/binutils-2.24.tar.gz
$ tar -xzf binutils-2.24.tar.gz
$ make BINUTILS=binutils-2.24 ARCH=amd64 CFLAGS=-Wno-error
$ cp build/macosx-amd64/hsdis-amd64.dylib ../../../../../..
```
現在加上兩個flag -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly
```bash
$ java \
--module-path=graal/sdk/mxbuild/modules/org.graalvm.graal_sdk.jar:graal/truffle/mxbuild/modules/com.oracle.truffle.truffle_api.jar \
--upgrade-module-path=graal/compiler/mxbuild/modules/jdk.internal.vm.compiler.jar \
-XX:+UnlockExperimentalVMOptions \
-XX:+EnableJVMCI \
-XX:+UseJVMCICompiler \
-XX:-TieredCompilation \
-XX:+PrintCompilation \
-XX:+UnlockDiagnosticVMOptions \
-XX:+PrintAssembly \
-XX:CompileOnly=Demo::workload \
Demo
```
現在我們可以執行示例然後看看生成的加法指令
```bash
workload machine code: [15, 31, 68, 0, 0, 3, -14, -117, -58, -123, 5, ...]
...
0x000000010f71cda0: nopl 0x0(%rax,%rax,1)
0x000000010f71cda5: add %edx,%esi ;*iadd {reexecute=0 rethrow=0 return_oop=0}
; - Demo::workload@2 (line 10)
0x000000010f71cda7: mov %esi,%eax ;*ireturn {reexecute=0 rethrow=0 return_oop=0}
; - Demo::workload@3 (line 10)
0x000000010f71cda9: test %eax,-0xcba8da9(%rip) # 0x0000000102b74006
; {poll_return}
0x000000010f71cdaf: vzeroupper
0x000000010f71cdb2: retq
```
ok,為了檢驗我們是不是真的掌握了,我們修改一下加法的實現,用減法代替。我會修改generate方法的加法節點,然後生成減法
```java
class AddNode {
void generate(...) {
... gen.emitSub(op1, op2, false) ... // changed from emitAdd
}
}
```
![](https://img2020.cnblogs.com/blog/1654682/202005/1654682-20200504115333133-814765888.jpg)
執行之後會看到生成的機器程式碼陣列改變了,機器指令也改變了
```bash
workload mechine code: [15, 31, 68, 0, 0, 43, -14, -117, -58, -123, 5, ...]
0x0000000107f451a0: nopl 0x0(%rax,%rax,1)
0x0000000107f451a5: sub %edx,%esi ;*iadd {reexecute=0 rethrow=0 return_oop=0}
; - Demo::workload@2 (line 10)
0x0000000107f451a7: mov %esi,%eax ;*ireturn {reexecute=0 rethrow=0 return_oop=0}
; - Demo::workload@3 (line 10)
0x0000000107f451a9: test %eax,-0x1db81a9(%rip) # 0x000000010618d006
; {poll_return}
0x0000000107f451af: vzeroupper
0x0000000107f451b2: retq
```
所以這裡我們學到了什麼?graal真的接受一個位元組碼陣列,我們可以看到圖node是如何從那個陣列上建立的,我們可以看到機器程式碼是如何基於node生成的,然後機器指令是如何編碼的。我們可以看到我們能改變graal工作機制。
```bash
[26, 27, 96, -84] → [15, 31, 68, 0, 0, 43, -14, -117, -58, -123, 5, ...]
```
## 優化
現在我們知道了怎麼得到圖表示,圖node怎麼生成機器程式碼,現在再讓我們看看graal是怎麼優化圖讓它高效的。
一個優化階段(phase)是一個方法,它有修改圖的機會。你要寫一個優化階段需要實現下面的介面。
```java
interface Phase {
void run(Graph graph);
}
```
### 1. 規範化(Canonicalisation)
規範化意味著重排node,形成一個統一的表示,規範化還有些其他目的,不過這次ppt我們要說的是規範化意味著常量摺疊和節點簡化。
節點可以簡化自身,它們本身有一個canonical方法
```java
interface Node {
Node canonical();
}
```
讓我們看看負節點(negate node),負節點即一元減法操作。如果一個一元減法作用於另一個一元減法,那麼減法本身就會消除,只留下原始值,,即--x == x
```java
class NegateNode implements Node {
Node canonical() {
if (value instanceof NegateNode) {
return ((NegateNode) value).getValue();
} else {
return this;
}
}
}
```
![](https://img2020.cnblogs.com/blog/1654682/202005/1654682-20200504115630357-719778589.jpg)
這是個理解graal絕佳的來自。程式碼邏輯已經不能在簡化了。
如果你有一個java操作要簡化,你可以修改canonical方法實現。
### 2. 全域性值編號(global value numbering)
全域性值編號是一種移除冗餘程式碼的技術。這個例子中,a+b可以只計算一次,然後使用兩次計算得倒的值。
```java
int workload(int a, int b) {
return (a + b) * (a + b);
}
```
graal可以比較兩個node看它們是否相等。如果相等則簡化。graal的全域性值編號階段會迭代檢查每個節點是否和其他任何節點相等,,如果相等就會替換為另一個節點的拷貝。他把所有節點放入hash map,這樣可以高效完成。有點類似於node快取。
![](https://img2020.cnblogs.com/blog/1654682/202005/1654682-20200504115726817-2048649925.jpg)
![](https://img2020.cnblogs.com/blog/1654682/202005/1654682-20200504115732828-2061691443.jpg)
注意測試節點不是固定的,這意味著節點在某個確定的時間不能有副作用。如果我們使用方法呼叫代替,就變成了確定的
```java
int workload() {
return (getA() + getB()) * (getA() + getB());
}
```
![](https://img2020.cnblogs.com/blog/1654682/202005/1654682-20200504115757610-1361438807.jpg)
### 3. 鎖粗化(lock coarsening)
來看一個更復雜的例子。有時候我們會在一個小範圍內對同一個物件多次使用synchonrized,雖然可能不會有意為之,但是編譯器內聯優化可能會導致那種程式碼產生。
```java
void workload() {
synchronized (monitor) {
counter++;
}
synchronized (monitor) {
counter++;
}
}
```
我們可以對它進行去糖化,然後高效實現
```java
void workload() {
monitor.enter();
counter++;
monitor.exit();
monitor.enter();
counter++;
monitor.exit();
}
```
我們可以優化這段程式碼,只進出一次monitor而不是多次進出,這就是鎖粗化
```java
void workload() {
monitor.enter();
counter++;
counter++;
monitor.exit();
}
```
在graal中鎖粗化由 LockEliminationPhase這個階段實現。它的 run 方法檢視所有monitor退出節點,然後看它們是否後面馬上跟一個monitor 進入節點。如果後面確認使用了使用了相同的monitor,會移除它們,只留下一個monitor進/出節點。
```java
void run(StructuredGraph graph) {
for (monitorExitNode monitorExitNode : graph.getNodes(MonitorExitNode.class)) {
FixedNode next = monitorExitNode.next();
if (next instanceof monitorEnterNode) {
AccessmonitorNode monitorEnterNode = (AccessmonitorNode) next;
if (monitorEnterNode.object() == monitorExitNode.object()) {
monitorExitNode.remove();
monitorEnterNode.remove();
}
}
}
}
```
![](https://img2020.cnblogs.com/blog/1654682/202005/1654682-20200504115915932-535536019.jpg)
這樣做是值得的,因為少了額外monitor進/出意味著程式碼變少了,其實這裡還允許我們繼續優化,可以把兩個遞增組合起來變成+2
```java
void workload() {
monitor.enter();
counter += 2;
monitor.exit();
}
```
讓我們用IGV看看。可以看到原圖有兩對monitoer enter/exit,然後變成了一對,當優化phase執行之後,兩個遞增變成了一個加法。
![](https://img2020.cnblogs.com/blog/1654682/202005/1654682-20200504115949671-641520794.jpg)
![](https://img2020.cnblogs.com/blog/1654682/202005/1654682-20200504115955585-12921724