Javaには、switchステートメントがif elseステートメントよりも高速であると言う本がたくさんあります。しかし、スイッチがifよりも速い理由と言っているところは見つかりませんでした。
私は2つのアイテムのうち1つを選択する必要がある状況があります。次のいずれかの方法を使用できます
switch(item){
case BREAD:
//eat Bread
break;
default:
//leave the restaurant
}
または、次のようなifステートメントを使用します
if(item== BREAD){
//eat Bread
}else{
//leave the restaurant
}
アイテムとBREADは定数int値を考慮しています
上記の例では動作が速いのはなぜですか?
なぜなら、多くの場合に効率的なswitchステートメントの評価を可能にする特別なバイトコードがあるからです。
IF文で実装されている場合、チェック、次の句へのジャンプ、チェック、次の句へのジャンプなどがあります。スイッチを使用すると、JVMは値をロードして比較し、値テーブルを反復処理して一致を検出します。これはほとんどの場合高速です。
switch
ステートメントは、if
ステートメントよりも常に高速であるとは限りません。 switch
はすべての値に基づいてルックアップを実行できるため、if-else
ステートメントの長いリストよりも拡張性があります。ただし、短い条件の場合、それは速くならず、遅くなる可能性があります。
現在のJVMには、LookupSwitchとTableSwitchの2種類のスイッチバイトコードがあります。
Switchステートメントの各ケースには整数オフセットがあり、これらのオフセットが連続している場合(または大きなギャップのない大部分が連続している場合)(ケース0:ケース1:ケース2など)、TableSwitchが使用されます。
オフセットが大きなギャップで広がっている場合(ケース0:ケース400:ケース93748:など)、LookupSwitchが使用されます。
要するに、可能な値の範囲内の各値には特定のバイトコードオフセットが与えられるため、TableSwitchは一定の時間で実行されるという違いです。したがって、ステートメントに3のオフセットを指定すると、正しい分岐を見つけるために3にジャンプすることがわかります。
ルックアップスイッチは、バイナリ検索を使用して正しいコードブランチを見つけます。これはO(log n)時間で実行されますが、これはまだ良いですが、最良ではありません。
詳細については、こちらを参照してください。 JVMのLookupSwitchとTableSwitchの違い?
したがって、どちらが最も速いかについては、このアプローチを使用してください。値が連続的またはほぼ連続的であるケースが3つ以上ある場合は、常にスイッチを使用してください。
2つのケースがある場合は、ifステートメントを使用します。
その他の状況では、スイッチは最も可能性が高いより高速ですが、LookupSwitchのバイナリ検索が悪いシナリオにヒットする可能性があるため、保証されません。
また、JVMはコード内で最もホットなブランチを最初に配置しようとするifステートメントでJIT最適化を実行することに注意してください。これは「分岐予測」と呼ばれます。詳細については、こちらをご覧ください: https://dzone.com/articles/branch-prediction-in-Java
あなたの経験は異なる場合があります。 JVMがLookupSwitchで同様の最適化を実行しないことは知りませんが、JIT最適化を信頼し、コンパイラを裏切ることはしないことを学びました。
そのため、最近ではパケットの負荷を計画している場合、メモリはそれほど大きなコストではなく、アレイは非常に高速です。また、switchステートメントを使用してジャンプテーブルを自動生成することもできないため、ジャンプテーブルシナリオを自分で簡単に生成できます。以下の例でわかるように、最大255パケットを想定しています。
以下の結果を得るには、抽象化が必要です。それがどのように機能するかを説明するつもりはありません。
これを更新して、(id <0)の境界チェックを行う必要がある場合、パケットサイズを255に設定します|| (ID>長さ)。
Packets[] packets = new Packets[255];
static {
packets[0] = new Login(6);
packets[2] = new Logout(8);
packets[4] = new GetMessage(1);
packets[8] = new AddFriend(0);
packets[11] = new JoinGroupChat(7); // etc... not going to finish.
}
public void handlePacket(IncomingData data)
{
int id = data.readByte() & 0xFF; //Secure value to 0-255.
if (packet[id] == null)
return; //Leave if packet is unhandled.
packets[id].execute(data);
}
C++でジャンプテーブルを頻繁に使用するので編集して、関数ポインタージャンプテーブルの例を示します。これは非常に一般的な例ですが、私はそれを実行しましたが、正しく動作します。ポインターをNULLに設定する必要があることに注意してください。C++はJavaのようにこれを自動的に行いません。
#include <iostream>
struct Packet
{
void(*execute)() = NULL;
};
Packet incoming_packet[255];
uint8_t test_value = 0;
void A()
{
std::cout << "I'm the 1st test.\n";
}
void B()
{
std::cout << "I'm the 2nd test.\n";
}
void Empty()
{
}
void Update()
{
if (incoming_packet[test_value].execute == NULL)
return;
incoming_packet[test_value].execute();
}
void InitializePackets()
{
incoming_packet[0].execute = A;
incoming_packet[2].execute = B;
incoming_packet[6].execute = A;
incoming_packet[9].execute = Empty;
}
int main()
{
InitializePackets();
for (int i = 0; i < 512; ++i)
{
Update();
++test_value;
}
system("pause");
return 0;
}
また、もう1つのポイントは、有名なDivide and Conquerです。したがって、255以上の配列のアイデアは、最悪の場合のシナリオとしてifステートメントを8個まで減らすことができます。
つまりしかし、それは面倒で速く管理するのが難しくなり、他のアプローチは一般的に優れていることを覚えておいてください、しかしこれは配列がそれをカットしない場合に利用されます。ユースケースと、各状況が最適なタイミングを把握する必要があります。少数のチェックしかない場合、これらのアプローチのどちらも使用したくないのと同じです。
If (Value >= 128)
{
if (Value >= 192)
{
if (Value >= 224)
{
if (Value >= 240)
{
if (Value >= 248)
{
if (Value >= 252)
{
if (Value >= 254)
{
if (value == 255)
{
} else {
}
}
}
}
}
}
}
}
バイトコードレベルでは、サブジェクト変数は、ランタイムによってロードされた構造化.classファイルのメモリアドレスからプロセッサレジスタに一度だけロードされ、これはswitchステートメントにあります。一方、if文では、コードコンパイルDEによって異なるjvm命令が生成されます。これにより、次の前のif文と同じ変数が使用されますが、各変数をレジスタにロードする必要があります。アセンブリ言語でのコーディングを知っている場合、これは当たり前のことです。 Javaコンパイルされたcoxはバイトコードまたは直接的なマシンコードではありませんが、この条件付きの概念はまだ一貫しています。まあ、私は説明する際により深い専門性を避けようとしました。コンセプトを明確にして分かりやすくしたことを願っています。ありがとうございました。