スイッチがなぜより速いのか


116

多くのJavaの本では、このswitch声明は声明よりも速いと説明されていますif else。しかし、なぜスイッチがより速いのかはどこにもわかりませんでした。

2つのうち1つを選ばなければならない状況があります。どちらでも使えます

switch (item) {
    case BREAD:
        //eat Bread
        break;
    default:
        //leave the restaurant
}

または

if (item == BREAD) {
    //eat Bread
} else {
    //leave the restaurant
}

アイテムとBREADを考慮すると、定数のint値です。

上記の例で、どちらがより高速で、なぜですか?


たぶんこれは、Javaのためにも答えである:stackoverflow.com/questions/767821/...
トビアス

19
一般的に、Wikipediaから:入力値の範囲が識別可能に「小さい」ため、ギャップがわずかしかない場合、オプティマイザーを組み込んだ一部のコンパイラーは、実際にはswitchステートメントをブランチテーブルまたはインデックス付き関数ポインターの配列として実装する場合があります。長い一連の条件付き命令。これにより、switchステートメントは、比較のリストを調べなくても、実行する分岐を即座に決定できます。
Felix Kling、2011

この質問の一番上の答えは、それをかなりよく説明しています。この記事では、すべてについてかなりよく説明しています。
bezmax '15

ほとんどの状況で、最適化コンパイラーが同様のパフォーマンス特性を持つコードを生成できることを願っています。いずれの場合も、違いに気付くために何百万回も電話をかける必要があります。
ミッチウィート

2
説明や証明、推論なしでこのような発言をする本には注意が必要です。
matt b

回答:


110

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

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


6
反復は「チェック、ジャンプ」に変換されませんか?
fivetwentysix 2011

17
@fivetwentysix:いいえ、情報についてはこちらを参照してください:artima.com/underthehood/flowP.html。記事からの引用:JVMがtableswitch命令に遭遇すると、JVMはキーが低と高で定義された範囲内にあるかどうかを確認するだけです。そうでない場合は、デフォルトのブランチオフセットが使用されます。そうである場合、キーからlowを減算して、オフセットをブランチオフセットのリストに取得します。このようにして、各ケース値をチェックする必要なく、適切なブランチオフセットを決定できます。
bezmax

1
(i)a switchtableswitchバイトコード命令に変換されない場合があります-これはlookupswitchif / elseと同様に実行される命令になる場合があります(ii)tableswitchバイトコード命令であっても、要因に応じてJITによって一連のif / elseにコンパイルされる場合がありますcases の数など。
アッシリア14年


34

switch声明は、より速く、常にではありませんif声明。すべての値に基づいてルックアップを実行できるため、if-elseステートメントの長いリストよりもスケーリングが優れswitchています。ただし、短時間の状態の場合は、それより速くなることはなく、遅くなる可能性があります。


5
「長い」を制約してください。5より大きいですか?10より大きいですか?20〜30以上ですか?
vanderwyst 2012年

11
場合によります。私にとっては、3つ以上の提案switchは、高速ではないにしてもより明確になります。
Peter Lawrey、2012年

どのような条件下で遅くなる可能性がありますか?
Eric

1
@Eric少数のesp Stringまたはintのスパースな値の場合は遅くなります。
Peter Lawrey

8

現在の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のバイナリ検索が悪いシナリオにヒットする可能性があるため、保証はありません。

また、最もホットなブランチをコードの最初に配置しようとするifステートメントでJVMがJIT最適化を実行することにも注意してください。これを「分岐予測」といいます。この詳細については、こちらをご覧くださいhttps : //dzone.com/articles/branch-prediction-in-java

あなたの経験は異なる場合があります。JVMがLookupSwitchで同様の最適化を実行しないことはわかりませんが、JIT最適化を信頼し、コンパイラーを裏切らないようにすることを学びました。


1
これを投稿してから、おそらくJava 12で「スイッチ式」と「パターンマッチング」がJavaに登場することに気づきました 。openjdk.java.net / jeps / 325 openjdk.java.net/jeps/305 まだ具体的なものはありませんが、これらはswitchさらに強力な言語機能になると思われます。たとえば、パターンマッチングにより、はるかにスムーズでパフォーマンスの高いinstanceof検索が可能になります。ただし、基本的な切り替え/ ifシナリオについては、前述のルールが引き続き適用されると想定しても安全だと思います。
HesNotTheStig 2018

1

したがって、パケットのロードを計画している場合、メモリは最近の大きなコストではなく、配列はかなり高速です。また、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 {

                              }
                          }
                      }
                  }
             }      
        }
   }
}

2
なぜ二重間接なのか?とにかくIDを制約する必要があるので、着信IDを境界チェックして0 <= id < packets.length確認packets[id]!=nullし、実行するだけではどうpackets[id].execute(data)ですか。
Lawrence Dol 2014

ええ、遅い応答でもう一度これを見て申し訳ありません。そして、私はポストlolを更新してパケットを符号なしバイトのサイズに制限したので、長さのチェックは必要ありません。
ジェレミートリフィロ

0

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

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.