前回はGraalでの大域的値番号付けでした。
こちらの記事に沿ってGraalの理解を深めます。
Understanding How Graal Works - a Java JIT Compiler Written in Java
今回はロック粗粒化(Lock coarsening)です。同一のモニタに対して続けて2回同期化しているコードがあるとします。直接そういうコードは書かないでしょうが、インライン化の結果として実際にこうなる場合があります。
void workload() { synchronized (monitor) { counter++; } synchronized (monitor) { counter++; } }
de-sugar(脱糖)するとこんなイメージです。
void workload() {
monitor.enter();
counter++;
monitor.exit();
monitor.enter();
counter++;
monitor.exit();
}
exit()してすぐenter()するので、最適化としてはモニタに入るのを1回だけにすればいいわけです。
void workload() {
monitor.enter();
counter++;
counter++;
monitor.exit();
}
これがロック粗粒化です。
Graalでのロック粗粒化
前回書きましたが、Graalのグラフを変更する処理はorg.graalvm.compiler.phases.Phase
インタフェースの実装クラスです。ロック粗粒化はorg.graalvm.compiler.phases.common.LockEliminationPhase
クラスです。
@Override protected void run(StructuredGraph graph) { for (MonitorExitNode monitorExitNode : graph.getNodes(MonitorExitNode.TYPE)) { FixedNode next = monitorExitNode.next(); if ((next instanceof MonitorEnterNode || next instanceof RawMonitorEnterNode)) { // should never happen, osr monitor enters are always direct successors of the graph // start assert !(next instanceof OSRMonitorEnterNode); AccessMonitorNode monitorEnterNode = (AccessMonitorNode) next; if (GraphUtil.unproxify(monitorEnterNode.object()) == GraphUtil.unproxify(monitorExitNode.object())) { /* * We've coarsened the lock so use the same monitor id for the whole region, * otherwise the monitor operations appear to be unrelated. */ MonitorIdNode enterId = monitorEnterNode.getMonitorId(); MonitorIdNode exitId = monitorExitNode.getMonitorId(); if (enterId != exitId) { enterId.replaceAndDelete(exitId); } GraphUtil.removeFixedWithUnusedInputs(monitorEnterNode); GraphUtil.removeFixedWithUnusedInputs(monitorExitNode); } } } }
MonitorExitNodeを探し、あればMonitorExitNodeのnext()がMonitorEnterNodeかを調べます。で、GraphUtil.removeFixedWithUnusedInputs
で不要なMonitorEnterNodeとMonitorExitNodeを削除します。先ほどの内容通りのことを実装しています。厳密にはモニタの対象が同じでも、モニタIDが異なる場合があるのでその場合はまず同一のIDに置き換えるケースがあります。
ロック粗粒化自体は単純ですが、その結果さらなる最適化に取り組めます。
void workload() {
monitor.enter();
counter++;
counter++;
monitor.exit();
}
counter++
が2回あるので、
void workload() { monitor.enter(); counter += 2; monitor.exit(); }
IGVで見る
ロック粗粒化をIGVで見ます。サンプルとして以下のコードを実行します。
class Demo4 { int counter; public static void main(String[] args) { Demo4 d = new Demo4(); while (true) { d.workload(); } } void workload() { synchronized (this) { counter++; } synchronized (this) { counter++; } } }
$ java \ --module-path=/Users/jyukutyo/code/graal/sdk/mxbuild/modules/org.graalvm.graal_sdk:/Users/jyukutyo/code/graal/truffle/mxbuild/modules/com.oracle.truffle.truffle_api.jar \ --upgrade-module-path=/Users/jyukutyo/code/graal/compiler/mxbuild/modules/jdk.internal.vm.compiler \ -XX:+UnlockExperimentalVMOptions \ -XX:+EnableJVMCI \ -XX:+UseJVMCICompiler \ -XX:-TieredCompilation \ -XX:+PrintCompilation \ -XX:CompileOnly=Demo4::workload \ -XX:CompileCommand=quiet \ -Dgraal.Dump \ Demo4
Javaバイトコードをパースしたあとでは、MonitorEnterとExitが2回あります。
各フェーズがapplyされたあとでは、MonitorEnter(RawMonitorEnter)とExitが1回となり、+も2回ではなくC(2)を1回+するだけとなっています。