「切り替え」は「if」より高速ですか?


242

switchステートメントは実際にはステートメントよりも高速ifですか?

以下のコードをVisual Studio 2010のx64 C ++コンパイラーで/Oxフラグを付けて実行しました。

#include <stdlib.h>
#include <stdio.h>
#include <time.h>

#define MAX_COUNT (1 << 29)
size_t counter = 0;

size_t testSwitch()
{
    clock_t start = clock();
    size_t i;
    for (i = 0; i < MAX_COUNT; i++)
    {
        switch (counter % 4 + 1)
        {
            case 1: counter += 4; break;
            case 2: counter += 3; break;
            case 3: counter += 2; break;
            case 4: counter += 1; break;
        }
    }
    return 1000 * (clock() - start) / CLOCKS_PER_SEC;
}

size_t testIf()
{
    clock_t start = clock();
    size_t i;
    for (i = 0; i < MAX_COUNT; i++)
    {
        const size_t c = counter % 4 + 1;
        if (c == 1) { counter += 4; }
        else if (c == 2) { counter += 3; }
        else if (c == 3) { counter += 2; }
        else if (c == 4) { counter += 1; }
    }
    return 1000 * (clock() - start) / CLOCKS_PER_SEC;
}

int main()
{
    printf("Starting...\n");
    printf("Switch statement: %u ms\n", testSwitch());
    printf("If     statement: %u ms\n", testIf());
}

そしてこれらの結果を得た:

Switchステートメント:5261 ms
Ifステートメント:5196 ms

私が学んだことから、switchステートメントは分岐テーブルを最適化するためにジャンプテーブルを使用しているようです。

質問:

  1. x86またはx64では、基本的なジャンプテーブルはどのようになりますか?

  2. このコードはジャンプテーブルを使用していますか?

  3. この例でパフォーマンスに違いがないのはなぜですか?パフォーマンスに大きな違いある状況はありますか?


コードの分解:

testIf:

13FE81B10 sub  rsp,48h 
13FE81B14 call qword ptr [__imp_clock (13FE81128h)] 
13FE81B1A mov  dword ptr [start],eax 
13FE81B1E mov  qword ptr [i],0 
13FE81B27 jmp  testIf+26h (13FE81B36h) 
13FE81B29 mov  rax,qword ptr [i] 
13FE81B2E inc  rax  
13FE81B31 mov  qword ptr [i],rax 
13FE81B36 cmp  qword ptr [i],20000000h 
13FE81B3F jae  testIf+0C3h (13FE81BD3h) 
13FE81B45 xor  edx,edx 
13FE81B47 mov  rax,qword ptr [counter (13FE835D0h)] 
13FE81B4E mov  ecx,4 
13FE81B53 div  rax,rcx 
13FE81B56 mov  rax,rdx 
13FE81B59 inc  rax  
13FE81B5C mov  qword ptr [c],rax 
13FE81B61 cmp  qword ptr [c],1 
13FE81B67 jne  testIf+6Dh (13FE81B7Dh) 
13FE81B69 mov  rax,qword ptr [counter (13FE835D0h)] 
13FE81B70 add  rax,4 
13FE81B74 mov  qword ptr [counter (13FE835D0h)],rax 
13FE81B7B jmp  testIf+0BEh (13FE81BCEh) 
13FE81B7D cmp  qword ptr [c],2 
13FE81B83 jne  testIf+89h (13FE81B99h) 
13FE81B85 mov  rax,qword ptr [counter (13FE835D0h)] 
13FE81B8C add  rax,3 
13FE81B90 mov  qword ptr [counter (13FE835D0h)],rax 
13FE81B97 jmp  testIf+0BEh (13FE81BCEh) 
13FE81B99 cmp  qword ptr [c],3 
13FE81B9F jne  testIf+0A5h (13FE81BB5h) 
13FE81BA1 mov  rax,qword ptr [counter (13FE835D0h)] 
13FE81BA8 add  rax,2 
13FE81BAC mov  qword ptr [counter (13FE835D0h)],rax 
13FE81BB3 jmp  testIf+0BEh (13FE81BCEh) 
13FE81BB5 cmp  qword ptr [c],4 
13FE81BBB jne  testIf+0BEh (13FE81BCEh) 
13FE81BBD mov  rax,qword ptr [counter (13FE835D0h)] 
13FE81BC4 inc  rax  
13FE81BC7 mov  qword ptr [counter (13FE835D0h)],rax 
13FE81BCE jmp  testIf+19h (13FE81B29h) 
13FE81BD3 call qword ptr [__imp_clock (13FE81128h)] 
13FE81BD9 sub  eax,dword ptr [start] 
13FE81BDD imul eax,eax,3E8h 
13FE81BE3 cdq       
13FE81BE4 mov  ecx,3E8h 
13FE81BE9 idiv eax,ecx 
13FE81BEB cdqe      
13FE81BED add  rsp,48h 
13FE81BF1 ret       

testSwitch:

13FE81C00 sub  rsp,48h 
13FE81C04 call qword ptr [__imp_clock (13FE81128h)] 
13FE81C0A mov  dword ptr [start],eax 
13FE81C0E mov  qword ptr [i],0 
13FE81C17 jmp  testSwitch+26h (13FE81C26h) 
13FE81C19 mov  rax,qword ptr [i] 
13FE81C1E inc  rax  
13FE81C21 mov  qword ptr [i],rax 
13FE81C26 cmp  qword ptr [i],20000000h 
13FE81C2F jae  testSwitch+0C5h (13FE81CC5h) 
13FE81C35 xor  edx,edx 
13FE81C37 mov  rax,qword ptr [counter (13FE835D0h)] 
13FE81C3E mov  ecx,4 
13FE81C43 div  rax,rcx 
13FE81C46 mov  rax,rdx 
13FE81C49 inc  rax  
13FE81C4C mov  qword ptr [rsp+30h],rax 
13FE81C51 cmp  qword ptr [rsp+30h],1 
13FE81C57 je   testSwitch+73h (13FE81C73h) 
13FE81C59 cmp  qword ptr [rsp+30h],2 
13FE81C5F je   testSwitch+87h (13FE81C87h) 
13FE81C61 cmp  qword ptr [rsp+30h],3 
13FE81C67 je   testSwitch+9Bh (13FE81C9Bh) 
13FE81C69 cmp  qword ptr [rsp+30h],4 
13FE81C6F je   testSwitch+0AFh (13FE81CAFh) 
13FE81C71 jmp  testSwitch+0C0h (13FE81CC0h) 
13FE81C73 mov  rax,qword ptr [counter (13FE835D0h)] 
13FE81C7A add  rax,4 
13FE81C7E mov  qword ptr [counter (13FE835D0h)],rax 
13FE81C85 jmp  testSwitch+0C0h (13FE81CC0h) 
13FE81C87 mov  rax,qword ptr [counter (13FE835D0h)] 
13FE81C8E add  rax,3 
13FE81C92 mov  qword ptr [counter (13FE835D0h)],rax 
13FE81C99 jmp  testSwitch+0C0h (13FE81CC0h) 
13FE81C9B mov  rax,qword ptr [counter (13FE835D0h)] 
13FE81CA2 add  rax,2 
13FE81CA6 mov  qword ptr [counter (13FE835D0h)],rax 
13FE81CAD jmp  testSwitch+0C0h (13FE81CC0h) 
13FE81CAF mov  rax,qword ptr [counter (13FE835D0h)] 
13FE81CB6 inc  rax  
13FE81CB9 mov  qword ptr [counter (13FE835D0h)],rax 
13FE81CC0 jmp  testSwitch+19h (13FE81C19h) 
13FE81CC5 call qword ptr [__imp_clock (13FE81128h)] 
13FE81CCB sub  eax,dword ptr [start] 
13FE81CCF imul eax,eax,3E8h 
13FE81CD5 cdq       
13FE81CD6 mov  ecx,3E8h 
13FE81CDB idiv eax,ecx 
13FE81CDD cdqe      
13FE81CDF add  rsp,48h 
13FE81CE3 ret       

更新:

ここで興味深い結果。ただし、なぜ高速で低速なのかはわかりません。


47
この考えを終わらせるために投票している人々はいったい何なのでしょうか?彼らは完全に最適化されたコンパイラーの概念を信じているので、理想的なコードよりも少ないコードを生成するという考えは異端だと思いますか?非常にアイデアん任意の最適化はどこでもそれらを怒らせますか?
Crashworks

6
この質問の何が問題になっていますか?
Tugrul Ates 2011

25
この質問の何が悪いのか疑問に思う方へ:まず第一に、それは質問ではなく3つの質問です。つまり、多くの回答がさまざまな問題に対処しているということです。これは、すべてに答える答えを受け入れるのが難しいことを意味します。さらに、上記の質問に対する典型的な反応は、このレベルの最適化ではほとんど常に時期尚早に最適化しているため、「それほど興味深いものではない」と締めくくります。最後に、5196対5261は、実際に気にするのに十分ではありません。意味のある論理コードを記述します。
Lasse V. Karlsen、2011

40
@ラッセ:本当に SOに3つの質問を投稿することを本当に望んでいませんか?また:5196 vs. 5261 shouldn't be enough to actually care->質問を誤解しているかどうか、またはコメントを誤解しているかどうかはわかりませんが、私の質問の全体の目的が違いがないのかどうかを尋ねることではありませんか?(これが重要な違いだと私が主張したことはありますか?)
user541686

5
@Robert:メタコメントなので、コメントは20件以上しかありません。この質問に実際に関連するコメントは7つだけです。意見:ここに「意見」があるかどうかはわかりません。あります理由ノー、パフォーマンスの違いを見ていないよということは?味だけ?議論:たぶん、でも、SOの他の場所で見たように、それは私にとって健康的な議論のように見えます(それに反するものがあれば教えてください)。議論:ここでは議論の余地はありません(「議論」の同義語として取り上げている場合を除きます)。拡張ディスカッション:これらのメタコメントを含める場合。
user541686 '25 / 07/25

回答:


122

コンパイラースイッチに対して行うことができるいくつかの最適化があります。よく言われる "ジャンプテーブル"は、入力が何らかの方法で制限できる場合にのみ機能するため、非常に便利なものではないと思います。

「ジャンプテーブル」のためのC擬似コードは次のようなものになるだろう。この練習では、コンパイラは、入力がテーブルに有効であったことを確実にするためにテーブルの周りにあれば、テストのいくつかのフォームを挿入する必要があることに注意してください- 。また、入力が連続した数値の連続である特定の場合にのみ機能することにも注意してください。

スイッチ内のブランチの数が非常に多い場合、コンパイラーはスイッチの値に対してバイナリー検索を使用するようなことを行うことができます。シナリオは、スイッチと同じくらい一般的であり、生成されるコードサイズが大きくなることはありません。しかし、それを確認するには、テストコードで違いを確認するためにさらに多くのブランチが必要になります。

特定の質問に答えるには:

  1. クランのような1つのそのルックス生成し、これを

    test_switch(char):                       # @test_switch(char)
            movl    %edi, %eax
            cmpl    $19, %edi
            jbe     .LBB0_1
            retq
    .LBB0_1:
            jmpq    *.LJTI0_0(,%rax,8)
            jmp     void call<0u>()         # TAILCALL
            jmp     void call<1u>()         # TAILCALL
            jmp     void call<2u>()         # TAILCALL
            jmp     void call<3u>()         # TAILCALL
            jmp     void call<4u>()         # TAILCALL
            jmp     void call<5u>()         # TAILCALL
            jmp     void call<6u>()         # TAILCALL
            jmp     void call<7u>()         # TAILCALL
            jmp     void call<8u>()         # TAILCALL
            jmp     void call<9u>()         # TAILCALL
            jmp     void call<10u>()        # TAILCALL
            jmp     void call<11u>()        # TAILCALL
            jmp     void call<12u>()        # TAILCALL
            jmp     void call<13u>()        # TAILCALL
            jmp     void call<14u>()        # TAILCALL
            jmp     void call<15u>()        # TAILCALL
            jmp     void call<16u>()        # TAILCALL
            jmp     void call<17u>()        # TAILCALL
            jmp     void call<18u>()        # TAILCALL
            jmp     void call<19u>()        # TAILCALL
    .LJTI0_0:
            .quad   .LBB0_2
            .quad   .LBB0_3
            .quad   .LBB0_4
            .quad   .LBB0_5
            .quad   .LBB0_6
            .quad   .LBB0_7
            .quad   .LBB0_8
            .quad   .LBB0_9
            .quad   .LBB0_10
            .quad   .LBB0_11
            .quad   .LBB0_12
            .quad   .LBB0_13
            .quad   .LBB0_14
            .quad   .LBB0_15
            .quad   .LBB0_16
            .quad   .LBB0_17
            .quad   .LBB0_18
            .quad   .LBB0_19
            .quad   .LBB0_20
            .quad   .LBB0_21
  2. ジャンプテーブルを使用していないと言えます。4つの比較手順が明確に表示されています。

    13FE81C51 cmp  qword ptr [rsp+30h],1 
    13FE81C57 je   testSwitch+73h (13FE81C73h) 
    13FE81C59 cmp  qword ptr [rsp+30h],2 
    13FE81C5F je   testSwitch+87h (13FE81C87h) 
    13FE81C61 cmp  qword ptr [rsp+30h],3 
    13FE81C67 je   testSwitch+9Bh (13FE81C9Bh) 
    13FE81C69 cmp  qword ptr [rsp+30h],4 
    13FE81C6F je   testSwitch+0AFh (13FE81CAFh) 

    ジャンプテーブルベースのソリューションは、比較をまったく使用しません。

  3. コンパイラがジャンプテーブルを生成するのに十分な分岐がないか、コンパイラがそれらを生成しないだけです。どちらかわかりません。

EDIT 2014:LLVMオプティマイザーに詳しい人々から、ジャンプテーブルの最適化が多くのシナリオで重要になる可能性があるという他の議論がありました。たとえば、多くの値を持つ列挙があり、その列挙の値に対する多くの場合があります。とは言っても、2011年に私が上で言ったことを支持します-多くの場合、「切り替えた場合、ケースがいくつあっても同じ時間になるだろう」と考える人がよくいますが、それは完全に誤りです。ジャンプテーブルを使用した場合でも、間接ジャンプコストが発生し、ケースごとにテーブルのエントリに対して支払います。また、メモリ帯域幅は、最新のハードウェアでは大きな問題です。

読みやすいようにコードを記述します。ソルトに値するコンパイラは、if / else ifはしごを見て同等のスイッチに変換するか、そうする方が速い場合はその逆になります。


3
+1は、実際に質問に答えるため、および有用な情報のためです。:-)しかし、質問:私が理解しているところによると、ジャンプテーブルは間接ジャンプを使用しています。あれは正しいですか?もしそうなら、プリフェッチ/パイプライン処理がより難しくなるため、通常は遅くなりませんか?
user541686 2011

1
@Mehrdad:はい、間接ジャンプを使用します。ただし、1つの間接ジャンプ(付属のパイプラインストールを伴う)は、数百の直接ジャンプよりも少ない場合があります。:)
ビリー・オニール

1
@Mehrdad:いいえ、残念ながら。:(IFの方が読みやすいといつも思っている人々の
集まりに

1
いくつかのquips-"[スイッチ]は、入力が何らかの方法で制限できる場合にのみ機能します" "テーブルの周りに何らかの形式のifテストを挿入して、入力がテーブルで有効であることを確認する必要があります。また、特定の入力が一連の連続した数値である場合。 ":潜在的にポインタが読み取られ、NULL以外の値がジャンプされた場合にのみ、データがまばらに入力されたテーブルを持つことは完全に可能です。それ以外の場合は、デフォルトのケースにジャンプします。その後、switch出口。ソレンは、この答えを読んだ後に私が言いたかった他のいくつかのことを言った。
Tony Delroy、

2
「ソルトに値するコンパイラは、if / else ifはしごを見て、同等のスイッチに変換するか、またはその逆になります」-このアサーションのサポートはありますか?コンパイラは、if節の順序が頻度と相対的なパフォーマンスのニーズに合わせてすでに手動で調整されていると想定する場合がありますswitch。過去を飛び越えて良い点switch:-)。コードサイズはケース/範囲に依存します-良いかもしれません。最後に、一部の列挙型、ビットフィールド、およびcharシナリオは、本質的に有効/制限付きでオーバーヘッドがありません。
Tony Delroy、2014

47

あなたの質問に:

1. x86またはx64では、基本的なジャンプテーブルはどのようになりますか?

ジャンプテーブルは、配列構造などのラベルへのポインタを保持するメモリアドレスです。次の例は、ジャンプテーブルのレイアウトを理解するのに役立ちます

00B14538  D8 09 AB 00 D8 09 AB 00 D8 09 AB 00 D8 09 AB 00  Ø.«.Ø.«.Ø.«.Ø.«.
00B14548  D8 09 AB 00 D8 09 AB 00 D8 09 AB 00 00 00 00 00  Ø.«.Ø.«.Ø.«.....
00B14558  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
00B14568  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................

ここに画像の説明を入力してください

ここで、00B14538はJump tableへのポインターであり、D8 09 AB 00のような値はラベルポインターを表します。

2.このコードはジャンプテーブルを使用していますか? この場合、いいえ。

3.この例でパフォーマンスに違いがないのはなぜですか?

どちらの場合も命令が同じに見えるため、パフォーマンスに違いはありません。ジャンプテーブルもありません。

4.パフォーマンスに大きな違いがある状況はありますか?

ifチェックのシーケンスが非常に長い場合、その場合、ジャンプテーブルを使用するとパフォーマンスが向上します(分岐/ jmp命令は、ほぼ完全に予測しないとコスト高くなります)が、メモリのコストが伴います。

すべての比較命令のコードにもある程度のサイズがあるため、特に32ビットポインターまたはオフセットを使用すると、単一のジャンプテーブルのルックアップで実行可能ファイルのサイズが大幅に増えるとは限りません。

結論:コンパイラーはそのようなケースを処理し、適切な命令を生成するのに十分スマートです:)


(編集:nvm、ビリーの答えはすでに私が提案していたことを持っています。これは素晴らしい補足だと思います。)gcc -S出力を含めるとよいでしょう:.long L1/ .long L2テーブルエントリのシーケンスは、16進ダンプよりも意味があり、そのような人にとってより有用ですコンパイラの見方を学びたい。(私はあなたがそれが間接的なjmpであるか、またはjccの束であるかどうかを確認するために、スイッチのコードを見ていたと思いますが)。
Peter Cordes

31

コンパイラーは、ifステートメントと同等のコードとしてswitchステートメントを自由にコンパイルしたり、ジャンプテーブルを作成したりできます。コンパイラオプションで指定した内容に応じて、何が最も速く実行されるか、または最小のコードが生成されるかに基づいて、どちらかを選択する可能性があります-最悪の場合、ifステートメントと同じ速度になります

私はコンパイラが最良の選択を行い、コードを最も読みやすくすることに焦点を当てると信頼しています。

ケースの数が非常に大きくなると、ジャンプテーブルは一連のifよりもはるかに高速になります。ただし、値の間のステップが非常に大きい場合、ジャンプテーブルが大きくなる可能性があり、コンパイラはそれを生成しないことを選択する場合があります。


13
これがOPの質問に答えるとは思いません。全然。
ビリーONeal

5
@ソーレン:それが「基本的な質問」だったとしたら、質問の他の179行は気にならなかったでしょう。1行だけだったでしょう。:-)
user541686

8
@ソーレン:OPの質問の一部として、少なくとも3つの番号付きサブ質問が表示されます。すべての「パフォーマンス」の質問に適用されるまったく同じ答え、つまり最初に測定する必要のある答えを単にトランペットで演奏しただけです。Mehrdadがすでに測定を行っており、このコードをホットスポットに分離していると考えてください。そのような場合、あなたの答えは価値がないより悪く、それはノイズです。
ビリーONeal

2
ジャンプテーブルとは何であるかと、定義に依存しないものとの間にぼやけた線があります。サブ質問のパート3についての情報を提供しました
。–ソレン

2
@wnoise:それが唯一の正しい答えであれば、パフォーマンスに関する質問をする理由は決してありません。ただし、実際にソフトウェアを測定している人がいます。測定したコードを高速化する方法がわからない場合があります。Mehrdadが質問する前にこの質問にいくらかの努力を払ったことは明らかです。そして私は彼の具体的な質問は答えられる以上のものだと思います。
Billy ONeal、2011

13

コンピュータがスイッチテストループ中にテストに関係のないタスクを実行しておらず、ifテストループ中に実行するタスクが少ないことをどのようにして確認しますか?テスト結果は次のようには表示されません。

  1. 違いは非常に小さい
  2. 結果は1つだけで、一連の結果はありません
  3. ケースが少なすぎる

私の結果:

私は追加しました:

printf("counter: %u\n", counter);

例ではカウンタが使用されなかったのでループを最適化しないように、コンパイラがループを実行するのはなぜですか?すぐに、そのようなマイクロベンチマークでもスイッチは常に勝っていました。

コードの他の問題は次のとおりです。

switch (counter % 4 + 1)

あなたのスイッチループでは、

const size_t c = counter % 4 + 1; 

あなたのifループで。あなたがそれを修正すると、非常に大きな違いがあります。switchステートメント内にステートメントを置くと、コンパイラが値をスタックに最初に置くのではなく、CPUレジスタに直接送信するようになると思います。したがって、これはswitchステートメントに有利であり、バランスの取れたテストではありません。

ああ、私はまた、テストの間にカウンターをリセットする必要があると思います。実際、おそらく何かを最適化するため、+ 1、+ 2、+ 3などではなく、ある種の乱数を使用する必要があります。乱数とは、たとえば現在の時刻に基づいた数値を意味します。そうしないと、コンパイラーが両方の関数を1つの長い数学演算に変換し、ループに煩わされることさえありません。

ライアンのコードを変更して、コードが実行される前にコンパイラーが何かを理解できないようにしました。

#include <stdlib.h>
#include <stdio.h>
#include <time.h>

#define MAX_COUNT (1 << 26)
size_t counter = 0;

long long testSwitch()
{
    clock_t start = clock();
    size_t i;
    for (i = 0; i < MAX_COUNT; i++)
    {
        const size_t c = rand() % 20 + 1;

        switch (c)
        {
                case 1: counter += 20; break;
                case 2: counter += 33; break;
                case 3: counter += 62; break;
                case 4: counter += 15; break;
                case 5: counter += 416; break;
                case 6: counter += 3545; break;
                case 7: counter += 23; break;
                case 8: counter += 81; break;
                case 9: counter += 256; break;
                case 10: counter += 15865; break;
                case 11: counter += 3234; break;
                case 12: counter += 22345; break;
                case 13: counter += 1242; break;
                case 14: counter += 12341; break;
                case 15: counter += 41; break;
                case 16: counter += 34321; break;
                case 17: counter += 232; break;
                case 18: counter += 144231; break;
                case 19: counter += 32; break;
                case 20: counter += 1231; break;
        }
    }
    return 1000 * (long long)(clock() - start) / CLOCKS_PER_SEC;
}

long long testIf()
{
    clock_t start = clock();
    size_t i;
    for (i = 0; i < MAX_COUNT; i++)
    {
        const size_t c = rand() % 20 + 1;
        if (c == 1) { counter += 20; }
        else if (c == 2) { counter += 33; }
        else if (c == 3) { counter += 62; }
        else if (c == 4) { counter += 15; }
        else if (c == 5) { counter += 416; }
        else if (c == 6) { counter += 3545; }
        else if (c == 7) { counter += 23; }
        else if (c == 8) { counter += 81; }
        else if (c == 9) { counter += 256; }
        else if (c == 10) { counter += 15865; }
        else if (c == 11) { counter += 3234; }
        else if (c == 12) { counter += 22345; }
        else if (c == 13) { counter += 1242; }
        else if (c == 14) { counter += 12341; }
        else if (c == 15) { counter += 41; }
        else if (c == 16) { counter += 34321; }
        else if (c == 17) { counter += 232; }
        else if (c == 18) { counter += 144231; }
        else if (c == 19) { counter += 32; }
        else if (c == 20) { counter += 1231; }
    }
    return 1000 * (long long)(clock() - start) / CLOCKS_PER_SEC;
}

int main()
{
    srand(time(NULL));
    printf("Starting...\n");
    printf("Switch statement: %lld ms\n", testSwitch()); fflush(stdout);
    printf("counter: %d\n", counter);
    counter = 0;
    srand(time(NULL));
    printf("If     statement: %lld ms\n", testIf()); fflush(stdout);
    printf("counter: %d\n", counter);
} 

スイッチ:3740
場合:3980

(複数回試行しても同様の結果)

また、ケース/ ifの数を5に減らしても、スイッチ機能は引き続き有効です。


Idk、私はそれを証明することはできません。異なる結果が得られますか?
user541686 2011

+1:ベンチマークは困難であり、通常のコンピューターで1回の実行でわずかな時間差から結論を出すことはできません。多数のテストを実行して、結果に対して統計を実行する場合があります。または、エミュレータでの制御された実行におけるプロセッササイクルのカウント。
Thomas Padron-McCarthy、

えっと、どこにprintステートメントを追加したのですか?プログラム全体の最後に追加しましたが、違いはありませんでした。また、他の問題との「問題」が理解できません...「非常に大きな違い」が何であるかを説明してください。
user541686 2011

1
@BobTurbo:45983493は12時間以上です。それはタイプミスでしたか?
Gus

1
素晴らしい、今私はまたそれをしに行かなければならない:)
BobTurbo '25 / 07/25

7

MSVCなどの優れた最適化コンパイラは、以下を生成できます。

  1. ケースが長距離に配置されている場合の簡単なジャンプテーブル
  2. ギャップが多い場合のスパース(2レベル)ジャンプテーブル
  3. ケースの数が少ない場合、または値が近接していない場合の一連のif
  4. ケースが間隔の狭い範囲のいくつかのグループを表す場合、上記の組み合わせ。

要するに、スイッチが一連のifsより遅いように見える場合、コンパイラーはそれを1つに変換するだけかもしれません。そして、それは単に各ケースの一連の比較ではなく、二分探索木になる可能性があります。例については、こちらをご覧ください。


実際、コンパイラーはハッシュとジャンプに置き換えることもできます。これは、提案する2レベルのスパースソリューションよりもパフォーマンスが優れています。
アリス

5

私は2)と答えて、いくつかの一般的なコメントをします。2)いいえ、投稿したアセンブリコードにはジャンプテーブルがありません。ジャンプテーブルは、ジャンプ先のテーブルと、テーブルからインデックス付きの場所に直接ジャンプするための1つまたは2つの命令です。ジャンプテーブルは、可能な切り替え先が多数ある場合により効果的です。おそらく、オプティマイザは、宛先の数がいくつかのしきい値を超えない限り、else if elseロジックの方が高速であることを知っています。4ではなく20の可能性で例をもう一度試してください。


#2への回答に+1をありがとう!:)(ところで、もっと可能性のある結果がここにあります。)
user541686

4

私は興味をそそられ、switchステートメントをより速く実行するために、あなたの例について何を変更できるかを調べました。

40個のifステートメントに到達し、0ケースを追加すると、ifブロックは同等のswitchステートメントよりも実行が遅くなります。ここに結果があります:https : //www.ideone.com/KZeCz

0ケースを削除した場合の影響は、https//www.ideone.com/LFnrXで確認できます


1
リンクが壊れています。
TS

4

これは、古い(現在は見つけるのが難しい)ベンチ++ベンチマークの結果です。

Test Name:   F000003                         Class Name:  Style
CPU Time:       0.781  nanoseconds           plus or minus     0.0715
Wall/CPU:        1.00  ratio.                Iteration Count:  1677721600
Test Description:
 Time to test a global using a 2-way if/else if statement
 compare this test with F000004

Test Name:   F000004                         Class Name:  Style
CPU Time:        1.53  nanoseconds           plus or minus     0.0767
Wall/CPU:        1.00  ratio.                Iteration Count:  1677721600
Test Description:
 Time to test a global using a 2-way switch statement
 compare this test with F000003

Test Name:   F000005                         Class Name:  Style
CPU Time:        7.70  nanoseconds           plus or minus      0.385
Wall/CPU:        1.00  ratio.                Iteration Count:  1677721600
Test Description:
 Time to test a global using a 10-way if/else if statement
 compare this test with F000006

Test Name:   F000006                         Class Name:  Style
CPU Time:        2.00  nanoseconds           plus or minus     0.0999
Wall/CPU:        1.00  ratio.                Iteration Count:  1677721600
Test Description:
 Time to test a global using a 10-way switch statement
 compare this test with F000005

Test Name:   F000007                         Class Name:  Style
CPU Time:        3.41  nanoseconds           plus or minus      0.171
Wall/CPU:        1.00  ratio.                Iteration Count:  1677721600
Test Description:
 Time to test a global using a 10-way sparse switch statement
 compare this test with F000005 and F000006

これからわか​​ることは、(このマシンでは、このコンパイラーを持つVC ++ 9.0 x64)、各ifテストに約0.7ナノ秒かかることです。テストの数が増えるにつれて、時間はほぼ完全に直線的にスケーリングされます。

switchステートメントを使用すると、値が密である限り、2ウェイテストと10ウェイテストの速度にほとんど違いがありません。スパース値を使用した10ウェイテストは、デンス値を使用した10ウェイテストの約1.6倍の時間がかかりますが、スパース値を使用した場合でも、10ウェイif/の2倍の速度よりも優れていelse ifます。

結論:4ウェイテストのみを使用してswitchvs if/のパフォーマンスについてはあまりわかりませんelse。このコードの数値を見ると、4ウェイテストの場合、2つのテストはかなり似た結果を生成することが予想されます(if/ elseは〜2.8ナノ秒、/ は〜2.0 ナノ秒switch)。


1
テストが意図的にif/ elseチェーンの最後で一致しない、または最後でのみ一致する値を探すのか、それらを分散するのかなどわからない場合は、何をどうしたらよいかわかりませんbench++。10以降にソースを見つけることができません。分グーグル。
Tony Delroy、2014

3

スイッチがジャンプテーブルにコンパイルされていない場合、スイッチよりも効率的である場合は非常に頻繁に書き込むことができます。

(1)ケースに順序付けがある場合、すべてのNのワーストケーステストではなく、上半分または下半分であるかどうかをテストするようにifを記述し、その各半分でバイナリ検索スタイル...最悪のケースはNではなくlogN

(2)特定のケース/グループが他のケースよりもはるかに頻繁である場合、それらのケースを最初に分離するようにifを設計すると、平均時間を短縮できます。


これは著しく誤りです。コンパイラーは、これらの最適化の両方を実行する能力以上のものです。
アリス

1
アリス、コンパイラはどのようにして、予想されるワークロードで他のケースよりも一般的に発生するケースを知るようになっていますか?(A:それはおそらく知ることができないので、そのような最適化を行うことはおそらくできません。)
ブライアン・ケネディ

(1)簡単に実行でき、一部のコンパイラでは、バイナリ検索を実行するだけで実行できます。(2)さまざまな方法で予測したり、コンパイラーに指示したりできます。GCCの「可能性が高い」または「可能性が低い」を使用したことはありませんか?
アリス

また、一部のコンパイラでは、統計を収集してその情報から最適化するモードでプログラムを実行できます。
Phil1970 2017

2

いいえ、そうでなければ、次にジャンプし、その後、ジャンプします...ジャンプテーブルには、アドレスのテーブルがあるか、ハッシュなどを使用します。

速いか遅いかは主観的です。たとえば、ケース1を最初ではなく最後にすることができます。また、テストプログラムまたは実際のプログラムがケース1を使用する場合、この実装ではほとんどの場合コードが遅くなります。したがって、実装に応じてケースリストを再配置するだけで、大きな違いが生まれます。

1〜4ではなく0〜3のケースを使用した場合、コンパイラはジャンプテーブルを使用している可能性があります。コンパイラは+1を削除することを考え出したはずです。おそらくそれは少数のアイテムでした。たとえば、それを0-15または0-31にした場合、テーブルで実装したか、他のショートカットを使用した可能性があります。コンパイラは、ソースコードの機能性を満たす限り、実装方法を自由に選択できます。そして、これはコンパイラの違いとバージョンの違い、そして最適化の違いに入ります。ジャンプテーブルが必要な場合はジャンプテーブルを作成し、if-then-elseツリーが必要な場合はif-then-elseツリーを作成します。コンパイラーに決定させたい場合は、switch / caseステートメントを使用します。


2

ただし、なぜ高速で低速なのかはわかりません。

これは実際に説明するのはそれほど難しくありません...誤って予測されたブランチは、正しく予測されたブランチよりも数十から数百倍も高価であることを覚えている場合。

では% 20、バージョン、最初のケースは/場合は、必ずヒット1つです。最近のCPUは、通常どの分岐が実行され、どの分岐が実行されないかを「学習」するため、ループのほぼすべての反復でこの分岐がどのように動作するかを簡単に予測できます。これが「if」バージョンが飛ぶ理由を説明しています。最初のテストの後に何も実行する必要はなく、ほとんどの反復でそのテストの結果を(正しく)予測します。明らかに、「スイッチ」の実装方法は少し異なります。計算されたブランチのおかげで遅くなる可能性のあるジャンプテーブルです。

では% 21バージョン、ブランチは、本質的にランダムです。したがって、それらの多くが反復ごとに実行されるだけでなく、CPUはそれらがどの方向に進むかを推測できません。これは、ジャンプテーブル(または他の「スイッチ」最適化)が役立つ可能性がある場合です。

最新のコンパイラとCPUでコードの一部がどのように実行されるかを予測することは非常に難しく、世代ごとに難しくなります。最善のアドバイスは、「気にしないで、常にプロファイリングする」ことです。その助言はますます良くなり、そしてそれを無視することができる人々のセットは毎年小さくなります。

これらすべては、上記の私の説明の大部分は推測に基づくものです。:-)


2
何百倍も遅いのはどこから来るのかわかりません。誤って予測されたブランチの最悪のケースはパイプラインのストールであり、最新のほとんどのCPUでは約20倍遅くなります。何百回も。(さて、古いNetBurstチップを使用している場合は35倍遅くなるかもしれません...)
Billy ONeal

@ビリー:わかりましたので、少し先を見ています。 Sandy Bridgeプロセッサでは、「各予測ミス分岐はパイプライン全体をフラッシュし、最大100程度の実行中の命令の作業を失います」。パイプラインは、一般的にすべての世代で本当に深くなります...
Nemo

1
違います。P4(NetBurst)には31のパイプラインステージがありました。Sandy Bridgeのステージは大幅に少なくなっています。「100命令ぐらいの作業を失う」というのは、命令キャッシュが無効化されるという前提の下にあると思います。実際に発生する一般的な間接ジャンプの場合、ジャンプテーブルのようなものの場合、間接ジャンプのターゲットは命令キャッシュのどこかにある可能性があります。
ビリーONeal '25

@ビリー:私は同意しないと思います。私の声明は次のとおりです。「誤って予測されたブランチは、正しく予測されたブランチよりも数十から数百倍高価です」。少し大げさかもしれませんが、Iキャッシュと実行パイプラインの深さでヒットするだけではありません。私が読んだものから、デコードのみのキューは約20命令です。
ニモ

分岐予測ハードウェアが実行パスを誤って予測した場合、命令パイプラインにある誤ったパスからのuopsは、実行を停止することなく、現在の場所に単純に削除されます。私はこれがどのようにして可能であるか(または私がそれを誤って解釈しているかどうか)わかりませんが、Nehalemで分岐が誤って予測されるパイプラインのストールはないようです。(
それでも

1

なし。アセンブラを使用してパフォーマンスの実際の測定を行うほとんどの特定のケースでは、あなたの質問は単に間違ったものです。与えられた例では、あなたの思考は決定的に短すぎます

counter += (4 - counter % 4);

私はあなたが使用するべき正しい増分式であるように見えます。

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