JavaバイトコードでLookUpSwitchとTableSwitchを理解するのが少し難しい。
私がよく理解している場合、LookUpSwitchとTableSwitchはどちらもJavaソースのswitch
ステートメントに対応しています。なぜ1つのJavaステートメントが2つの異なるバイトコードを生成するのですか?
それぞれのJasminドキュメント:
違いは、lookupswitchはキーとラベルのあるテーブルを使用しますが、tableswitchはラベルのみのテーブルを使用することです。
tableswitchを実行する場合、スタックの一番上のint値は、テーブルへのインデックスとして直接使用され、ジャンプ先を取得してすぐにジャンプを実行します。 lookup + jumpプロセス全体はO(1)演算であり、非常に高速です。
lookupswitchを実行する場合、スタックの最上部のint値は、一致が見つかるまでテーブル内のキーと比較され、このキーの隣のジャンプ先を使用してジャンプが実行されます。 lookupswitchテーブルは常にソートする必要がありますなので、すべてのX <Yに対してkeyX <keyYになるため、lookup + jumpプロセス全体はO(log n)操作がキーになりますバイナリ検索アルゴリズムを使用して検索されます(一致を見つけるため、または一致するキーがないことを確認するために、int値をすべての可能なキーと比較する必要はありません)。 O(log n)はO(1)よりも少し遅いですが、よく知られているアルゴリズムの多くはO(log n)であり、これらは通常高速であると考えられているため、問題ありません。でもO(n)またはO(n * log n)は、かなり良いアルゴリズムと見なされます(遅い/悪いアルゴリズムにはO(n ^ 2)、O(n ^ 3)があり、またはさらに悪い)。
どの命令を使用するかの決定は、コンパイラがどのようにcompact switchステートメントがそうであるかという事実に基づいて行われます。
switch (inputValue) {
case 1: // ...
case 2: // ...
case 3: // ...
default: // ...
}
上のスイッチは完全にコンパクトで、数字の「穴」がありません。コンパイラーは、次のようなテーブルスイッチを作成します。
tableswitch 1 3
OneLabel
TwoLabel
ThreeLabel
default: DefaultLabel
Jasminページの疑似コードはこれをかなりよく説明しています:
int val = pop(); // pop an int from the stack
if (val < low || val > high) { // if its less than <low> or greater than <high>,
pc += default; // branch to default
} else { // otherwise
pc += table[val - low]; // branch to entry in table
}
このコードは、このようなテーブルスイッチがどのように機能するかを明確に示しています。 val
はinputValue
であり、low
は1(スイッチの最小ケース値)であり、high
は3(スイッチの最大ケース値)です。 )。
いくつかの穴があっても、スイッチはコンパクトにすることができます。
switch (inputValue) {
case 1: // ...
case 3: // ...
case 4: // ...
case 5: // ...
default: // ...
}
上記のスイッチは「ほぼコンパクト」で、1つの穴しかありません。コンパイラーは次の命令を生成できます。
tableswitch 1 6
OneLabel
FakeTwoLabel
ThreeLabel
FourLabel
FiveLabel
default: DefaultLabel
; <...code left out...>
FakeTwoLabel:
DefaultLabel:
; default code
ご覧のとおり、コンパイラは2の偽のケース、FakeTwoLabel
を追加する必要があります。 2はスイッチの実際の値ではないため、FakeTwoLabelは実際にはデフォルトケースが配置されている場所でコードフローを正確に変更するラベルです。2の値は実際にはデフォルトケースを実行するためです。
そのため、コンパイラーがテーブルスイッチを作成するためにスイッチを完全にコンパクトにする必要はありませんが、少なくともコンパクトさにはかなり近いはずです。次のスイッチについて考えてみます。
switch (inputValue) {
case 1: // ...
case 10: // ...
case 100: // ...
case 1000: // ...
default: // ...
}
このスイッチはコンパクトに近いものではなく、100倍以上値よりも穴があります。これをスペアスイッチと呼びます。コンパイラは、このスイッチをテーブルスイッチとして表現するためにほぼ1000の偽のケースを生成する必要があります。その結果、巨大なテーブルが作成され、クラスファイルのサイズが大幅に増大します。これは実用的ではありません。代わりに、lookupswitchが生成されます。
lookupswitch
1 : Label1
10 : Label10
100 : Label100
1000 : Label1000
default : DefaultLabel
このテーブルには、1,000を超えるエントリではなく、5つのエントリしかありません。テーブルには4つの実数値があり、O(log 4)は2です(ここでのログは、2を基数とするため、10を底とするのではなく、2を底とするログです)。つまり、VM比較を最大2回実行してinputValueのラベルを見つけるか、結論に達すると、値がテーブルにないため、デフォルト値を実行する必要があります。テーブルに100のエントリがあったとしても、正しいラベルを見つけるためにVM最大で7つの比較が必要か、デフォルトのラベルにジャンプすることを決定します(7つの比較は100の比較よりはるかに少ないです。 、あなたは思いませんか?)。
したがって、これら2つの命令が交換可能であったり、2つの命令の理由に歴史的な理由があることはナンセンスです。 2つの異なる状況に対して2つの手順があります。1つはコンパクト値(最大速度用)のスイッチ用であり、もう1つはスペア値(最大速度ではなく、まだ十分な速度とすべての数値の穴に関係なく非常にコンパクトなテーブル表現)のスイッチ用です。
javac
1.8.0_45がswitch
をコンパイルする対象を決定する方法
どちらを使用するかを決定するには、javac
選択アルゴリズムを基礎として使用できます。
javac
のソースはlangtools
リポジトリにあることがわかっています。
次に、grepを実行します。
_hg grep -i tableswitch
_
そして最初の結果は langtools/src/share/classes/com/Sun/tools/javac/jvm/Gen.Java :
_// Determine whether to issue a tableswitch or a lookupswitch
// instruction.
long table_space_cost = 4 + ((long) hi - lo + 1); // words
long table_time_cost = 3; // comparisons
long lookup_space_cost = 3 + 2 * (long) nlabels;
long lookup_time_cost = nlabels;
int opcode =
nlabels > 0 &&
table_space_cost + 3 * table_time_cost <=
lookup_space_cost + 3 * lookup_time_cost
?
tableswitch : lookupswitch;
_
どこ:
hi
:最大ケース値lo
:最小ケース値したがって、時間と空間の両方の複雑さを考慮に入れ、時間の複雑さの重みは3であると結論付けます。
TODO O(log(n))でバイナリ検索を行うとtableswitch
を実行できるため、log(nlabels)
ではなく_lookup_time_cost = nlabels
_の理由がわかりません。
おまけの事実:C++コンパイラーもO(1)ジャンプテーブルとO(long(n))バイナリ検索の間で類似の選択を行います: ifの切り替えの利点-elseステートメント
Java仮想マシン仕様 違いを説明します。 「tableswitch命令は、スイッチのケースをターゲットオフセットのテーブルへのインデックスとして効率的に表現できる場合に使用されます。」仕様には、より詳細な説明があります。
Javaバイトコードとマシンコード(たとえば、Sun独自のCPU)に下線を引く)の特定のバインディングにより、ほとんどが歴史的なものだと思います。
Tableswitchは基本的に計算されたジャンプであり、宛先はルックアップテーブルから取得されます。逆に、lookupswitchでは、各値の比較が必要です。基本的には、一致する値が見つかるまで、テーブル要素の反復を繰り返します。
明らかにこれらのオペコードは交換可能ですが、値に基づいて、どちらかがより高速またはコンパクトになる可能性があります(たとえば、間に大きなギャップがある一連のキーと一連のキーのセットを比較します)。