web-dev-qa-db-ja.com

切り替えがifより速い理由

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値を考慮しています

上記の例では動作が速いのはなぜですか?

104
user831722

なぜなら、多くの場合に効率的なswitchステートメントの評価を可能にする特別なバイトコードがあるからです。

IF文で実装されている場合、チェック、次の句へのジャンプ、チェック、次の句へのジャンプなどがあります。スイッチを使用すると、JVMは値をロードして比較し、値テーブルを反復処理して一致を検出します。これはほとんどの場合高速です。

100
Daniel

switchステートメントは、ifステートメントよりも常に高速であるとは限りません。 switchはすべての値に基づいてルックアップを実行できるため、if-elseステートメントの長いリストよりも拡張性があります。ただし、短い条件の場合、それは速くならず、遅くなる可能性があります。

31
Peter Lawrey

現在の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最適化を信頼し、コンパイラを裏切ることはしないことを学びました。

7
HesNotTheStig

そのため、最近ではパケットの負荷を計画している場合、メモリはそれほど大きなコストではなく、アレイは非常に高速です。また、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 {

                              }
                          }
                      }
                  }
             }      
        }
   }
}
1
Jeremy Trifilo

バイトコードレベルでは、サブジェクト変数は、ランタイムによってロードされた構造化.classファイルのメモリアドレスからプロセッサレジスタに一度だけロードされ、これはswitchステートメントにあります。一方、if文では、コードコンパイルDEによって異なるjvm命令が生成されます。これにより、次の前のif文と同じ変数が使用されますが、各変数をレジスタにロードする必要があります。アセンブリ言語でのコーディングを知っている場合、これは当たり前のことです。 Javaコンパイルされたcoxはバイトコードまたは直接的なマシンコードではありませんが、この条件付きの概念はまだ一貫しています。まあ、私は説明する際により深い専門性を避けようとしました。コンセプトを明確にして分かりやすくしたことを願っています。ありがとうございました。