switch
ステートメントは実際にはif
ステートメントよりも高速ですか?
/Ox
フラグを使用して、Visual Studio 2010のx64 C++コンパイラで次のコードを実行しました。
#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ミリ秒
Ifステートメント:5196ミリ秒
私が学んだことから、switch
ステートメントは明らかにジャンプテーブルを使用して分岐を最適化します。
X86またはx64では、基本的なジャンプテーブルはどのように見えますか?
このコードはジャンプテーブルを使用していますか?
この例にパフォーマンスの違いがないのはなぜですか? にパフォーマンスの大きな違いがある状況はありますか?
コードの分解:
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
興味深い結果 ここ 。ただし、なぜ1つが速く、1つが遅いのかはわかりません。
コンパイラーcanがスイッチに対して行う最適化がいくつかあります。よく言われる「ジャンプテーブル」は、入力を何らかの方法で制限できる場合にのみ機能するため、非常に便利なものではないと思います。
「ジャンプテーブル」のC疑似コードは this のようなものになります。実際には、コンパイラはテーブルをテストする何らかの形式を挿入して、テーブルで入力が有効であることを確認する必要があります。 。また、入力が連続した数字の連続であるという特定の場合にのみ機能することにも注意してください。
スイッチ内のブランチの数が非常に多い場合、コンパイラはスイッチの値でバイナリ検索を使用するなどのことを実行できます。シナリオは、スイッチと同じくらい一般的であり、生成されるコードサイズが大きくなることはありません。しかし、それを確認するには、違いを確認するためにテストコードにさらに多くのブランチが必要です。
特定の質問に答えるには:
Clangは this のようなものを生成します:
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
私はそれがジャンプテーブルを使用していないと言うことができます-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)
ジャンプテーブルベースのソリューションでは、比較はまったく使用されません。
EDIT 2014:LLVMオプティマイザーに精通している人々から、ジャンプテーブルの最適化が多くのシナリオで重要になる可能性があるとの議論がありました。例えば多くの値を持つ列挙があり、その列挙の値に対する多くのケースがある場合。そうは言っても、私は2011年に私が上記のことを支持します-「私がそれを切り替えれば、どんなに多くのケースがあったとしても同じ時間になるだろう」と人々が思うことはよくあります-それは完全に間違っています。ジャンプテーブルを使用しても、間接ジャンプコストが発生し、各ケースのテーブルのエントリに対して支払います。また、メモリ帯域幅は、最新のハードウェアにとって大きな問題です。
読みやすくするためにコードを書きます。 ソルトに値するコンパイラーは、if/else ifラダーを確認し、同等のスイッチに変換するか、高速化する場合はその逆になります
あなたの質問に:
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はジャンプテーブルへのポインターであり、D8 09 AB 00などの値はラベルポインターを表します。
2。このコードはジャンプテーブルを使用していますか?この場合はありません。
3。この例にパフォーマンスの違いがないのはなぜですか?
両方のケースの命令は同じに見え、ジャンプテーブルがないため、パフォーマンスの違いはありません。
4。パフォーマンスに大きな違いがある状況はありますか?
ifチェックの非常に長いシーケンスがある場合、その場合、ジャンプテーブルを使用するとパフォーマンスが向上します(分岐/ jmp命令はexpensive)ほぼ完全に予測できない場合)、メモリのコストが伴います。
すべての比較命令のコードもサイズがあるため、特に32ビットポインターまたはオフセットでは、1回のジャンプテーブルルックアップで実行可能ファイルのサイズが大きくなることはありません。
結論:コンパイラーはそのようなケースを処理し、適切な命令を生成するのに十分なスマートです:)
コンパイラーは、switchステートメントをifステートメントと同等のコードとして自由にコンパイルしたり、ジャンプテーブルを作成したりできます。コンパイラオプションで指定した内容に応じて、最速で実行されるものまたは最小のコードを生成するものに基づいて、どちらか一方を選択する可能性があります。したがって、最悪の場合、ifステートメントと同じ速度になります
コンパイラが最善の選択を行い、コードを最も読みやすくするものに焦点を当てると信頼しています。
ケースの数が非常に多くなると、ジャンプテーブルは一連のifよりもはるかに高速になります。ただし、値間のステップが非常に大きい場合、ジャンプテーブルが大きくなる可能性があり、コンパイラはジャンプテーブルを生成しないことを選択する場合があります。
スイッチテストループ中にテストに関係のないタスクをコンピューターが実行しておらず、ifテストループ中に実行しているタスクが少ないことをどのように確認しますか?テスト結果には、次のものは何も表示されません。
私の結果:
私は付け加えた:
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
if:3980
(複数回の試行で同様の結果)
また、ケース/ ifの数を5に減らしましたが、スイッチ機能はまだ勝ちました。
MSVCなどの優れた最適化コンパイラは、以下を生成できます。
要するに、スイッチが一連のifよりも遅いように見える場合、コンパイラはそれを単に1に変換するだけです。そして、各ケースの単なる比較のシーケンスではなく、バイナリ検索ツリーになる可能性があります。例については here を参照してください。
2)に答えて、一般的なコメントをします。 2)いいえ、投稿したアセンブリコードにジャンプテーブルはありません。ジャンプテーブルは、ジャンプ先のテーブルと、テーブルからインデックス付きの場所に直接ジャンプするための1つまたは2つの命令です。切り替え先が多数ある場合は、ジャンプテーブルの方が理にかなっています。たぶん、オプティマイザーは、宛先の数が何らかのしきい値を超えない限り、else if if logicの方が高速であるということを知っています。 4ではなく20の可能性を使用して例をもう一度試してください。
興味をそそられたので、switchステートメントをより速く実行するためにあなたの例について何を変更できるかを見てみました。
40個のifステートメントを取得し、0個のケースを追加すると、ifブロックの実行は同等のswitchステートメントよりも遅くなります。ここに結果があります: https://www.ideone.com/KZeCz 。
0のケースを削除する効果は、ここで見ることができます: https://www.ideone.com/LFnrX 。
古い(現在は見つけにくい)bench ++ベンチマークの結果の一部を次に示します。
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ウェイテストの速度にalmost違いはありません。スパース値を使用した10ウェイテストは、デンス値を使用した10ウェイテストの約1.6倍の時間がかかりますが、スパース値を使用しても、10ウェイif
/else if
の2倍の速度よりも優れています。
結論:4方向のテストのみを使用しても、switch
対if
/else
のパフォーマンスについては実際には表示されませんmuch。このコードの数値を見ると、4ウェイテストの場合、2つがpretty同様の結果(if
に対して〜2.8ナノ秒)を生成することが予想されるという事実を簡単に補間できます/ else
、switch
の場合は〜2.0)。
スイッチがジャンプテーブルにコンパイルされていない場合、スイッチよりも効率的にifを書くことができることに注意してください...
(1)すべてのNについてテストする最悪のケースではなく、ケースに順序がある場合、ifまたはifをテストして上半分または下半分にテストし、その半分ごとにバイナリ検索スタイルで...最悪の場合は、N(2)特定のケース/グループが他のケースよりもはるかに頻繁に発生する場合、それらのケースを最初に分離するようにifを設計すると、平均時間を短縮できます。
ただし、なぜ速いのか、遅いのかはわかりません。
実際に説明するのはそれほど難しくありません...誤って予測されたブランチは、正しく予測されたブランチよりも数十倍から数百倍高いことを覚えているなら。
% 20
バージョンでは、最初のcase/ifが常にヒットします。最近のCPUは、通常どの分岐が使用され、どの分岐が使用されないかを「学習」するため、ループのほぼすべての反復でこの分岐がどのように動作するかを簡単に予測できます。 「if」バージョンが飛ぶ理由を説明しています。最初のテストを過ぎて何も実行する必要はなく、ほとんどの反復でそのテストの結果を(正しく)予測します。明らかに、「スイッチ」の実装はわずかに異なります。おそらくジャンプテーブルでさえ、計算されたブランチのおかげで遅くなる可能性があります。
% 21
バージョンでは、ブランチは本質的にランダムです。したがって、それらの多くがすべての反復を実行するだけでなく、CPUはどちらに進むかを推測できません。これは、ジャンプテーブル(または他の「スイッチ」最適化)が役立つ可能性が高い場合です。
最新のコンパイラとCPUでコードがどのように実行されるかを予測することは非常に難しく、世代を重ねるごとに難しくなります。最善のアドバイスは、「試行錯誤することなく、常にプロファイルを作成する」ことです。そのアドバイスはより良くなり、それを無視することができる人々の集合は毎年小さくなります。
これらすべては、上記の私の説明はほとんど推測であると言うことです。 :-)
これらは、もし他のジャンプなら、他のジャンプなら...ジャンプテーブルはアドレスのテーブルを持っているか、ハッシュまたはそのようなものを使用しています。
速くても遅くても主観的です。たとえば、ケース1を最初ではなく最後にすることができます。テストプログラムまたは実際のプログラムでほとんどの場合、この実装ではコードが遅くなります。そのため、実装に応じてケースリストを再配置するだけで大きな違いが得られます。
1〜4ではなく0〜3のケースを使用した場合、コンパイラはジャンプテーブルを使用した可能性がありますが、コンパイラはとにかく+1を削除する必要があります。おそらくそれは少数のアイテムだったのでしょう。たとえば、0〜15または0〜31にした場合、テーブルで実装したり、他のショートカットを使用した可能性があります。コンパイラは、ソースコードの機能を満たす限り、実装方法を自由に選択できます。そして、これはコンパイラの違い、バージョンの違い、最適化の違いになります。ジャンプテーブルが必要な場合は、ジャンプテーブルを作成します。if-then-elseツリーが必要な場合は、if-then-elseツリーを作成します。コンパイラに決定させる場合は、switch/caseステートメントを使用します。
無し。アセンブラにアクセスして実際のパフォーマンスを測定するほとんどの特定のケースでは、質問は間違っています。与えられた例では、あなたの思考は決定的に短すぎます
counter += (4 - counter % 4);
私はあなたが使用すべき正しい増分式であるように見えます。