switch
ケースプログラムがあります。
昇順のスイッチケース:
int main()
{
int a, sc = 1;
switch (sc)
{
case 1:
a = 1;
break;
case 2:
a = 2;
break;
}
}
コードのアセンブリ:
main:
Push rbp
mov rbp, rsp
mov DWORD PTR [rbp-4], 1
mov eax, DWORD PTR [rbp-4]
cmp eax, 1
je .L3
cmp eax, 2
je .L4
jmp .L2
.L3:
mov DWORD PTR [rbp-8], 1
jmp .L2
.L4:
mov DWORD PTR [rbp-8], 2
nop
.L2:
mov eax, 0
pop rbp
ret
降順のスイッチケース:
int main()
{
int a, sc = 1;
switch (sc)
{
case 2:
a = 1;
break;
case 1:
a = 2;
break;
}
}
コードのアセンブリ:
main:
Push rbp
mov rbp, rsp
mov DWORD PTR [rbp-4], 1
mov eax, DWORD PTR [rbp-4]
cmp eax, 1
je .L3
cmp eax, 2
jne .L2
mov DWORD PTR [rbp-8], 1
jmp .L2
.L3:
mov DWORD PTR [rbp-8], 2
nop
.L2:
mov eax, 0
pop rbp
ret
ここで、昇順注文の場合、降順注文よりも多くのアセンブリが生成されました。
スイッチケースの数がもっと多い場合、ケースの順序はパフォーマンスに影響しますか?
最適化されていないコードを見ているため、パフォーマンスを調べることはあまり意味がありません。例のoptimizedコードを見ると、まったく比較が行われていないことがわかります!オプティマイザーは、スイッチ変数sc
の値が常に_1
_であることに気付き、到達不能な_case 2
_を削除します。
オプティマイザーは、変数a
が割り当てられた後に使用されないことも認識するため、_case 1
_のコードも削除し、main()
を空の関数のままにします。また、そのレジスタは使用されていないため、rbp
を操作する関数prolog/epilogを削除します。
したがって、最適化されたコードは、main()
関数のどちらのバージョンでも同じになります。
_main:
xor eax, eax
ret
_
つまり、問題のコードについては、case
ステートメントを配置する順序は関係ありません。そのコードはまったく生成されないためです。
コードが実際に生成され使用される実際の例では、case
順序は重要ですか?おそらくない。 noptimized生成されたコードでも、両方のバージョンが2つのcase
値を数値順にテストし、最初に_1
_をチェックし、次に_2
_をチェックします。ソースコードの順序に関係なく。明らかに、コンパイラは最適化されていないコードでもソートを実行しています。
GlennとLundinのコメントに注意してください:case
セクションのorderが2つの例間の唯一の変更ではなく、実際のコードも異なります。それらの1つでは、ケース値はa
に設定された値と一致しますが、他の値はそうではありません。
コンパイラは、使用される実際の値に応じて、switch
/case
ステートメントにさまざまな戦略を使用します。これらの例のような一連の比較、またはジャンプテーブルを使用する場合があります。生成されたコードを調べるのは面白いかもしれませんが、いつものように、パフォーマンスが重要な場合は、最適化設定を監視し、実際の状況でtestを見てください。
switch
ステートメントのコンパイラー最適化 は注意が必要です。もちろん、最適化を有効にする必要があります(たとえば、 [〜#〜]でgcc -O2 -fverbose-asm -S
でコードをコンパイルしてみてくださいgcc [〜#〜] で、生成された.s
アセンブラファイルを確認します)。両方の例については、Debian/Sid/x86-64での私のGCC 7は単純に次のようになります:
.type main, @function
main:
.LFB0:
.cfi_startproc
# rsp.c:13: }
xorl %eax, %eax #
ret
.cfi_endproc
(したがって、その生成されたコードにはswitch
の痕跡はありません)
コンパイラがswitch
を最適化する方法を理解する必要がある場合は、 this など、その主題に関するいくつかの論文があります。
スイッチケースの数が多い場合、ケースの順序はパフォーマンスに影響しますか?
Not、最適化コンパイラを使用して最適化を要求する場合 this も参照してください。
それがあなたにとって非常に重要な場合(しかし、コンパイラにマイクロ最適化を任せてはいけません!)、生成されたアセンブラコードをベンチマークし、プロファイルし、おそらく研究する必要があります。ところで、 cache misses と register allocation はcase
- sの順序よりもはるかに重要である可能性があるので、気にしないでくださいすべて。最近のコンピューターのおおよその タイミング推定 に留意してください。 case
sを最も読解可能な順序で配置します(next開発者向け同じソースコード)。 スレッドコード についてもお読みください。 case
- sを並べ替える客観的な(パフォーマンスに関連する)理由がある場合(これは非常にまれであり、生涯に1回しか発生しません)、それらの理由を説明するコメントを書いてください。
パフォーマンスを重視する場合は、 benchmark および profile を確認し、適切なコンパイラーを選択して、関連する最適化オプションとともに使用してください。おそらく、いくつかの異なる 最適化 設定(およびおそらくいくつかのコンパイラ)を試してください。 -march=native
(-O2
または-O3
に加えて)を追加できます。コンパイルとリンクを-flto -O2
で検討して、リンク時の最適化などを有効にすることもできます。 プロファイルベース の最適化も必要になる場合があります。 。
ところで、多くのコンパイラは巨大な フリーソフトウェア プロジェクトです(特に [〜#〜] gcc [〜#〜] および Clang )。パフォーマンスを重視する場合は、コンパイラにパッチを適用し、ソースコードに forking を追加して最適化パスを追加し、GCCに プラグインを追加して、コンパイラを拡張することができます。 またはいくつかの GCC MELT 拡張機能)。それには数か月または数年の作業が必要です(特にそのコンパイラーの内部表現と構成を理解するため)。
(開発コストを考慮することを忘れないでください。ほとんどの場合、はるかに多くのコストがかかります)
パフォーマンスは、ケースの総数ではなく、主に特定のデータセットのブランチミスの数に依存します。そしてそれは、実際のデータとコンパイラーがスイッチを実装する方法に大きく依存します(ディスパッチテーブル、連鎖条件、条件のツリー-これをCから制御できるかどうかは不明です)。
Switch文は通常、単純な比較ではなく jump tables を介してコンパイルされます。
そのため、ケースステートメントを変更してもパフォーマンスは低下しません。
ただし、場合によっては、実行の流れを次のケースに進めてコードの重複を避けるために、より多くのケースを連続した順序で保持し、一部のエントリでブレーク/リターンを使用しないことが有用な場合があります。
case 10:
やcase 200000:
のように、ケースnumber
の数の違いがケースごとに大きい場合、コンパイラーは確実にジャンプテーブルを生成しません。ほとんどすべてのエントリはdefault:
の場合へのポインタを持ち、この場合は比較を使用します。
ほとんどのケースラベルが連続している場合、コンパイラは多くの場合、switch文を処理して比較ではなくジャンプテーブルを使用します。使用する計算されたジャンプの形式(存在する場合)をコンパイラが決定する正確な手段は、実装ごとに異なります。 switchステートメントに余分なケースを追加すると、コンパイラの生成コードを簡素化してパフォーマンスが向上する場合があります(たとえば、コードがケース4-11を使用し、ケース0-3がデフォルトの方法で処理され、明示的なcase 0:; case 1:; case 2:; case 3:;
前に default:
は、コンパイラがオペランドを12と比較し、それより少ない場合は12項目のジャンプテーブルを使用する場合があります。これらのケースを省略すると、コンパイラーは4を減算してから8と比較し、8項目のテーブルを使用する場合があります。
Switchステートメントを最適化しようとする際の難しさの1つは、コンパイラーは一般にプログラマーよりも特定の入力が与えられた場合にさまざまなアプローチのパフォーマンスがどのように変化するかをよく知っていることですが、プログラマーはコンパイラーよりもプログラムがどのような入力を受け取るかを知っている場合があります。次のようなものを考えます:
if (x==0)
y++;
else switch(x)
{
...
}
「スマート」コンパイラーは、コードを次のように変更することを認識します。
switch(x)
{
case 0:
y++;
break;
...
}
x
がゼロの場合、計算されたジャンプを犠牲にして、x
がゼロ以外のすべてのケースで比較を排除できます。 x
がほとんどの場合0以外であれば、それは良い取引です。ただし、x
がゼロの99.9%の場合、それは悪い取引である可能性があります。コンパイラの作成者が異なれば、前者のような構成を後者に最適化しようとする程度が異なります。
あなたの質問は非常に簡単です-あなたのコードは同じではないので、同じアセンブリを生成しません!最適化されたコードは、個々のステートメントだけでなく、その周りのすべてのものにも依存します。そしてこの場合、最適化を説明するのは簡単です。
最初の例では、ケース1はa = 1になり、ケース2はa = 2になります。コンパイラーはこれを最適化して、これら2つのケースに対してa = scを設定できます。これは単一ステートメントです。
2番目の例では、ケース1はa = 2になり、ケース2はa = 1になります。コンパイラはそのショートカットを使用できなくなったため、両方の場合に明示的にa = 1またはa = 2を設定する必要があります。もちろん、これにはさらにコードが必要です。
最初の例を取り上げて、ケースの順序を入れ替えただけの場合と条件コードの場合、同じアセンブラを取得する必要があります。
コードを使用してこの最適化をテストできます
int main()
{
int a, sc = 1;
switch (sc)
{
case 1:
case 2:
a = sc;
break;
}
}
また、まったく同じアセンブラーを提供する必要があります。
ちなみに、テストコードはscが実際に読み取られることを前提としています。ほとんどの最新の最適化コンパイラは、scが割り当てとswitchステートメントの間で変わらないことを発見し、scを定数値1に置き換えます。さらに最適化すると、switchステートメントの冗長な分岐が削除され、 aは実際には変更されないため、割り当てを最適化することができます。また、変数aの観点から、コンパイラはaが他の場所で読み込まれていないことを発見し、その変数をコードから完全に削除する場合があります。
Scを読み取り、aを設定したい場合は、volatile
の両方を宣言する必要があります。幸いなことに、コンパイラは期待どおりに実装しているように見えますが、最適化を有効にした場合、これを絶対に期待することはできません。
おそらくアセンブリコードを比較する前にコンパイラの最適化を有効にする必要がありますが、問題はコンパイル時に変数がわかっているため、コンパイラは副作用がないため関数からすべてを削除できることです。
この例 は、例のswitchステートメントでケースの順序を変更しても、最適化が有効になっている場合、GCCおよび他のほとんどのコンパイラーがそれらの順序を変更することを示しています。 extern関数を使用して、値が実行時にのみ認識されるようにしましたが、たとえばRand
も使用できます。
また、さらにケースを追加すると、コンパイラーは条件付きジャンプを関数のアドレスを含むテーブルに置き換えることができ、GCCによって並べ替えられます here .