JVM/javacは十分に賢いのだろうか
// This line...
string a = foo();
string foo()
{
return bar();
}
string bar()
{
return some-complicated-string computation;
}
に
string a = bar();
または、リリースケースでfoo()への不要な呼び出しを削除します(到達不能コードのため):
string a = foo(bar());
// bar is the same
...
string foo(string b)
{
if (debug) do-something-with(b);
}
最初の例では「はい」、2番目の例では「よくわからない」と感じていますが、それを確認するためのポインタやリンクを誰かに教えてもらえますか?
javac
は、バイトコードを生成した元のJavaプログラムを忠実に表現したバイトコードを表示します(最適化できる特定の状況を除く:定数畳み込みおよびデッドコード除去)。ただし、JVMがを使用する場合、最適化はJVMによって実行される場合があります。 JITコンパイラ。
最初のシナリオでは、JVMがインライン化をサポートしているように見えます(メソッドここ および ここ を参照) = JVMのインライン化の例の場合)。
javac
自体によって実行されているメソッドインライン化の例は見つかりませんでした。いくつかのサンプルプログラム(質問で説明したものと同様)をコンパイルしようとしましたが、final
の場合でも、メソッドを直接インライン化するものはなかったようです。この種の最適化は、javac
ではなく、JVMのJITコンパイラによって行われるように思われます。 Methodshere で説明されている「コンパイラ」は、javac
ではなくHotSpotJVMのJITコンパイラのようです。
私が見ることができることから、javac
はデッドコード除去(2番目のケースの例を参照)と定数畳み込み。定数畳み込みでは、コンパイラーは定数式を事前計算し、実行時に計算を実行する代わりに、計算された値を使用します。例えば:
public class ConstantFolding {
private static final int a = 100;
private static final int b = 200;
public final void baz() {
int c = a + b;
}
}
次のバイトコードにコンパイルされます。
Compiled from "ConstantFolding.Java"
public class ConstantFolding extends Java.lang.Object{
private static final int a;
private static final int b;
public ConstantFolding();
Code:
0: aload_0
1: invokespecial #1; //Method Java/lang/Object."<init>":()V
4: return
public final void baz();
Code:
0: sipush 300
3: istore_1
4: return
}
バイトコードには、aload
のgetfield
sとiadd
の代わりにsipush 300
があることに注意してください。 300
は計算値です。これは、private final
変数にも当てはまります。 a
とb
が静的でない場合、結果のバイトコードは次のようになります。
Compiled from "ConstantFolding.Java"
public class ConstantFolding extends Java.lang.Object{
private final int a;
private final int b;
public ConstantFolding();
Code:
0: aload_0
1: invokespecial #1; //Method Java/lang/Object."<init>":()V
4: aload_0
5: bipush 100
7: putfield #2; //Field a:I
10: aload_0
11: sipush 200
14: putfield #3; //Field b:I
17: return
public final void baz();
Code:
0: sipush 300
3: istore_1
4: return
}
ここでも、sipush 300
が使用されます。
2番目のケース(デッドコード除去)では、次のテストプログラムを使用しました。
public class InlineTest {
private static final boolean debug = false;
private void baz() {
if(debug) {
String a = foo();
}
}
private String foo() {
return bar();
}
private String bar() {
return "abc";
}
}
これにより、次のバイトコードが得られます。
Compiled from "InlineTest.Java"
public class InlineTest extends Java.lang.Object{
private static final boolean debug;
public InlineTest();
Code:
0: aload_0
1: invokespecial #1; //Method Java/lang/Object."<init>":()V
4: return
private void baz();
Code:
0: return
private Java.lang.String foo();
Code:
0: aload_0
1: invokespecial #2; //Method bar:()Ljava/lang/String;
4: areturn
private Java.lang.String bar();
Code:
0: ldc #3; //String abc
2: areturn
}
ご覧のとおり、foo
ブロック内のコードは事実上「デッド」であるため、baz
ではif
はまったく呼び出されません。
Sun(現在はOracle)のHotSpot JVMは、バイトコードの解釈とJITコンパイルを組み合わせたものです。バイトコードがJVMに提示されると、コードは最初に解釈されますが、JVMはバイトコードを監視し、頻繁に実行される部分を選択します。これらの部分をネイティブコードに変換して、実行速度を上げます。あまり頻繁に使用されないバイトコードの場合、このコンパイルは実行されません。コンパイルにはオーバーヘッドがあるため、これも同様です。ですから、それは本当にトレードオフの問題です。すべてのバイトコードをネイティブコードにコンパイルすることにした場合、コードの起動遅延が非常に長くなる可能性があります。
JVMは、バイトコードの監視に加えて、バイトコードを解釈およびロードするときにバイトコードの静的分析を実行して、さらに最適化を実行することもできます。
JVMが実行する特定の種類の最適化を知りたい場合は、Oracleの このページ が非常に役立ちます。 HotSpotJVMで使用されるパフォーマンス手法について説明します。
JVMはおそらくインラインになります。一般に、人間が読みやすいように最適化するのが最善です。 JVMにランタイム最適化を実行させます。
JVMエキスパート Brian Goetzによるとfinal
はインライン化されるメソッドに影響を与えません。
同じクラスファイルで、javacはstatic
とfinal
をインライン化できます(他のクラスファイルはインライン化された関数を変更する可能性があります)
ただし、JITはコードについて詳しく知っているため、より多くの最適化を行うことができます(余分な境界の削除やnullチェックのインライン化など)。
「高度に最適化された」JITコンパイラーは両方のケースをインライン化します(そして、@ Mysticialでは、さまざまな形式のトリックを使用することで、いくつかの多型ケースをインライン化することさえあります)。
メソッドをfinalにするなど、いくつかのトリックを行うことで、インライン化の可能性を高めることができます。
javacは、主にいくつかの条件付きコンパイルパラダイムを支援することを目的とした、主にfinal/privateメソッドのいくつかのプリミティブインライン化を行います。
Bar()で例外をスローし、スタックトレースを出力すると、呼び出しのパス全体が表示されます... Javaすべてを尊重すると思います。
2番目のケースも同じです。デバッグはシステムの変数であり、C++のように定義されていないため、事前に評価する必要があります。