コメントで既に述べたように、コレクション フレームワークと同時実行パッケージの主な作成者の 1 人である Doug Lea は、単なる人間にとっては紛らわしい (または直感に反する) ように見える最適化を行う傾向があります。
ここでの「有名な」例は、バイトコードのサイズを最小限に抑えるためにフィールドをローカル変数にコピーすることですtable。これは、実際には、参照した例のフィールドとローカルtab変数でも行われます!
非常に単純なテストでは、アクセスが「インライン化」されているかどうかに関係なく (結果のバイトコード サイズを参照して) 違いはないようです。だから私はあなたが言及したメソッドの構造に大まかに似た例を作成しようとしましたgetNode:配列であるフィールドへのアクセス、長さチェック、1つの配列要素のフィールドへのアクセス...
- メソッドは個別の
testSeparate割り当てとチェックを行います
- この
testInlinedメソッドは、代入式 if スタイルを使用します
- メソッドは
testRepeated(反例として)すべてのアクセスを繰り返します
コード:
class Node
{
int k;
int j;
}
public class AssignAndUseTestComplex
{
public static void main(String[] args)
{
AssignAndUseTestComplex t = new AssignAndUseTestComplex();
t.testSeparate(1);
t.testInlined(1);
t.testRepeated(1);
}
private Node table[] = new Node[] { new Node() };
int testSeparate(int value)
{
Node[] tab = table;
if (tab != null)
{
int n = tab.length;
if (n > 0)
{
Node first = tab[(n-1)];
if (first != null)
{
return first.k+first.j;
}
}
}
return 0;
}
int testInlined(int value)
{
Node[] tab; Node first, e; int n;
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1)]) != null) {
return first.k+first.j;
}
return 0;
}
int testRepeated(int value)
{
if (table != null)
{
if (table.length > 0)
{
if (table[(table.length-1)] != null)
{
return table[(table.length-1)].k+table[(table.length-1)].j;
}
}
}
return 0;
}
}
結果のバイトコード:testSeparateメソッドは41 の命令を使用します。
int testSeparate(int);
Code:
0: aload_0
1: getfield #15 // Field table:[Lstackoverflow/Node;
4: astore_2
5: aload_2
6: ifnull 40
9: aload_2
10: arraylength
11: istore_3
12: iload_3
13: ifle 40
16: aload_2
17: iload_3
18: iconst_1
19: isub
20: aaload
21: astore 4
23: aload 4
25: ifnull 40
28: aload 4
30: getfield #37 // Field stackoverflow/Node.k:I
33: aload 4
35: getfield #41 // Field stackoverflow/Node.j:I
38: iadd
39: ireturn
40: iconst_0
41: ireturn
メソッドはtestInlined確かに少し小さく、39 の命令があります。
int testInlined(int);
Code:
0: aload_0
1: getfield #15 // Field table:[Lstackoverflow/Node;
4: dup
5: astore_2
6: ifnull 38
9: aload_2
10: arraylength
11: dup
12: istore 5
14: ifle 38
17: aload_2
18: iload 5
20: iconst_1
21: isub
22: aaload
23: dup
24: astore_3
25: ifnull 38
28: aload_3
29: getfield #37 // Field stackoverflow/Node.k:I
32: aload_3
33: getfield #41 // Field stackoverflow/Node.j:I
36: iadd
37: ireturn
38: iconst_0
39: ireturn
最後に、このtestRepeatedメソッドはなんと63 の命令を使用します。
int testRepeated(int);
Code:
0: aload_0
1: getfield #15 // Field table:[Lstackoverflow/Node;
4: ifnull 62
7: aload_0
8: getfield #15 // Field table:[Lstackoverflow/Node;
11: arraylength
12: ifle 62
15: aload_0
16: getfield #15 // Field table:[Lstackoverflow/Node;
19: aload_0
20: getfield #15 // Field table:[Lstackoverflow/Node;
23: arraylength
24: iconst_1
25: isub
26: aaload
27: ifnull 62
30: aload_0
31: getfield #15 // Field table:[Lstackoverflow/Node;
34: aload_0
35: getfield #15 // Field table:[Lstackoverflow/Node;
38: arraylength
39: iconst_1
40: isub
41: aaload
42: getfield #37 // Field stackoverflow/Node.k:I
45: aload_0
46: getfield #15 // Field table:[Lstackoverflow/Node;
49: aload_0
50: getfield #15 // Field table:[Lstackoverflow/Node;
53: arraylength
54: iconst_1
55: isub
56: aaload
57: getfield #41 // Field stackoverflow/Node.j:I
60: iadd
61: ireturn
62: iconst_0
63: ireturn
したがって、クエリと割り当てを記述するこの「あいまいな」方法は、実際に数バイトのバイトコードを節約できるようであり、(ローカル変数へのフィールドの格納に関するリンクされた回答の正当性を考えると)これが使用する理由であった可能性がありますこのスタイル。
しかし...
いずれにせよ: メソッドが数回実行された後、JIT が作動し、結果のマシン コードは元のバイトコードとは「何も」関係ありません。3 つのバージョンすべてが実際には最終的に同じマシンコードにコンパイルされます。
つまり、結論は次のとおりです。このスタイルは使用しないでください。代わりに、読みやすく保守しやすいダム コードを記述してください。このような「最適化」を使用する番がいつになるかがわかります。
編集:短い補遺...
さらにテストを行い、JIT によって生成される実際のマシン コードtestSeparateについて、 andメソッドを比較しました。testInlined
非現実的な過度の最適化や JIT が取る可能性のあるその他の近道を避けるために、メソッドを少し変更しましたmainが、実際のメソッドは変更されていません。
予想どおり、ホットスポット逆アセンブリ JVM と でメソッドを数千回呼び出すと、-XX:+UnlockDiagnosticVMOptions -XX:+LogCompilation -XX:+PrintAssembly両方のメソッドの実際のマシン コードは同じになります。
繰り返しますが、JIT はその仕事をうまくこなし、プログラマーは(それが何を意味するにせよ)読みやすいコードを書くことに集中できます。
...そして、マイナーな修正/明確化:
3 番目の方法はテストしませんでした。これは、他の方法と同等testRepeatedではないためです(したがって、同じマシン コードを生成することはできません)。ところで、これは、フィールドをローカル変数に格納する戦略のもう 1 つの小さな利点です。これは、(非常に制限されていますが、時には便利な) 形式の "スレッド セーフ" を提供します: 配列の長さ (のメソッド内の配列は、メソッドの実行中に変更できません。tabgetNodeHashMap