switch
ステートメントを使用する場合とif
ステートメントを使用する場合のベストプラクティスは、30個のunsigned
列挙であり、約10個のアクションが期待されます(現在は同じアクションです)。パフォーマンスとスペースを考慮する必要がありますが、重要ではありません。スニペットを抽象化したので、命名規則を嫌わないでください。
switch
ステートメント:
// numError is an error enumeration type, with 0 being the non-error case
// fire_special_event() is a stub method for the shared processing
switch (numError)
{
case ERROR_01 : // intentional fall-through
case ERROR_07 : // intentional fall-through
case ERROR_0A : // intentional fall-through
case ERROR_10 : // intentional fall-through
case ERROR_15 : // intentional fall-through
case ERROR_16 : // intentional fall-through
case ERROR_20 :
{
fire_special_event();
}
break;
default:
{
// error codes that require no additional action
}
break;
}
if
ステートメント:
if ((ERROR_01 == numError) ||
(ERROR_07 == numError) ||
(ERROR_0A == numError) ||
(ERROR_10 == numError) ||
(ERROR_15 == numError) ||
(ERROR_16 == numError) ||
(ERROR_20 == numError))
{
fire_special_event();
}
スイッチを使用します。
最悪の場合、コンパイラはif-elseチェーンと同じコードを生成するため、何も失うことはありません。疑わしい場合は、最も一般的なケースを最初にswitchステートメントに入れてください。
最適な場合、オプティマイザーはコードを生成するより良い方法を見つけることがあります。コンパイラが行う一般的なことは、バイナリデシジョンツリーを構築する(平均的なケースで比較とジャンプを保存する)か、単にジャンプテーブルを構築する(比較することなく動作する)ことです。
例で提供した特殊なケースの場合、おそらく最も明確なコードは次のとおりです。
if (RequiresSpecialEvent(numError))
fire_special_event();
明らかにこれは問題をコードの別の領域に移動するだけですが、今ではこのテストを再利用する機会があります。また、それを解決するためのオプションもあります。たとえば、std :: setを使用できます。
bool RequiresSpecialEvent(int numError)
{
return specialSet.find(numError) != specialSet.end();
}
私は、これがオプションであるというだけで、RequiresSpecialEventの最適な実装であることを提案していません。それでも、スイッチまたはif-elseチェーン、ルックアップテーブル、または値のビット操作を使用できます。意思決定プロセスがあいまいになるほど、独立した機能に含めることで得られる価値が高まります。
スイッチはより高速です。
ループ内で30個の異なる値をif/else-ingしてみて、switchを使用して同じコードと比較し、スイッチの速度を確認してください。
さて、スイッチには1つの本当の問題があります:スイッチはコンパイル時に各ケース内の値を知らなければなりません。これは、次のコード:
// WON'T COMPILE
extern const int MY_VALUE ;
void doSomething(const int p_iValue)
{
switch(p_iValue)
{
case MY_VALUE : /* do something */ ; break ;
default : /* do something else */ ; break ;
}
}
コンパイルしません。
次に、ほとんどの人が定義(Aargh!)を使用し、他の人は同じコンパイル単位で定数変数を宣言および定義します。例えば:
// WILL COMPILE
const int MY_VALUE = 25 ;
void doSomething(const int p_iValue)
{
switch(p_iValue)
{
case MY_VALUE : /* do something */ ; break ;
default : /* do something else */ ; break ;
}
}
そのため、最終的に、開発者は「速度+明快さ」と「コード結合」を選択する必要があります。
(スイッチが地獄のように混乱するように書けないということではありません...私が現在見ているスイッチのほとんどは、この「混乱する」カテゴリーのものです...しかし、これは別の話です...)
2008-09-21の編集:
bk1e は次のコメントを追加しました: "これを処理する別の方法は、ヘッダーファイルで定数を列挙として定義することです」。
もちろん。
Extern型のポイントは、ソースから値を分離することでした。この値をマクロとして、単純なconst int宣言として、または列挙として定義すると、値をインライン化する副作用があります。したがって、定義、列挙値、またはconst int値が変更された場合、再コンパイルが必要になります。 extern宣言は、値が変更された場合に再コンパイルする必要がないことを意味しますが、スイッチの使用を不可能にします。結論は スイッチを使用すると、スイッチコードとケースとして使用される変数との結合が増加します。。 OKの場合、スイッチを使用します。そうでない場合は、驚きではありません。
。
編集2013-01-15:
Vlad Lazarenko 私の答えにコメントし、スイッチによって生成されたアセンブリコードの詳細な研究へのリンクを提供しました。非常に啓発的: http://lazarenko.me/switch/
コンパイラーはとにかくそれを最適化します-最も読みやすいのでスイッチに行きます。
読みやすさのみを目的とした場合のスイッチ。声明の維持が難しく、私の意見では読みにくい場合、巨大です。
ERROR_01://意図的なフォールスルー
または
(ERROR_01 == numError)
後者はエラーが発生しやすく、最初のものよりも多くの入力と書式設定が必要です。
読みやすくするためのコード。パフォーマンスの向上を知りたい場合は、プロファイラーを使用してください。最適化とコンパイラーはさまざまであり、パフォーマンスの問題は人々が考えているところではほとんどありません。
スイッチを使用してください、それはそれが目的であり、プログラマが期待するものです。
しかし、冗長なケースラベルを配置します-人々が安心できるようにするために、それらを除外するためのルールがいつ/何であるかを思い出そうとしました。
次のプログラマーがそれに取り組んで、言語の詳細について不必要な思考をする必要はありません(数か月後にはあなたになるかもしれません!)
コンパイラーは、switch
の最適化に非常に優れています。最近のgccは、if
内の一連の条件の最適化にも優れています。
godbolt でいくつかのテストケースを作成しました。
case
の値がグループ化されている場合、gcc、clang、iccはすべて、ビットマップを使用して値が特別な値の1つであるかどうかをチェックするのに十分スマートです。
例えばgcc 5.2 -O3はswitch
を(およびif
と非常によく似たものに)コンパイルします。
errhandler_switch(errtype): # gcc 5.2 -O3
cmpl $32, %edi
ja .L5
movabsq $4301325442, %rax # highest set bit is bit 32 (the 33rd bit)
btq %rdi, %rax
jc .L10
.L5:
rep ret
.L10:
jmp fire_special_event()
ビットマップは即時データであるため、データキャッシュにアクセスできない可能性や、ジャンプテーブルはありません。
gcc 4.9.2 -O3はswitch
をビットマップにコンパイルしますが、mov/shiftを使用して1U<<errNumber
をコンパイルします。 if
バージョンを一連のブランチにコンパイルします。
errhandler_switch(errtype): # gcc 4.9.2 -O3
leal -1(%rdi), %ecx
cmpl $31, %ecx # cmpl $32, %edi wouldn't have to wait an extra cycle for lea's output.
# However, register read ports are limited on pre-SnB Intel
ja .L5
movl $1, %eax
salq %cl, %rax # with -march=haswell, it will use BMI's shlx to avoid moving the shift count into ecx
testl $2150662721, %eax
jne .L10
.L5:
rep ret
.L10:
jmp fire_special_event()
errNumber
から1を引く方法に注意してください(lea
を使用して、その操作と移動を組み合わせます)。これにより、ビットマップを32ビットのイミディエイトに適合させ、より多くの命令バイトを使用する64ビットの即時movabsq
を回避できます。
短い(マシンコード内の)シーケンスは次のようになります。
cmpl $32, %edi
ja .L5
mov $2150662721, %eax
dec %edi # movabsq and btq is fewer instructions / fewer Intel uops, but this saves several bytes
bt %edi, %eax
jc fire_special_event
.L5:
ret
(jc fire_special_event
の使用の失敗は遍在し、 コンパイラのバグ です。)
rep ret
は、古いAMD K8およびK10(ブルドーザー前)の利点のために、分岐ターゲットで使用され、条件付き分岐に続きます: 「rep ret」とはどういう意味ですか 。これがないと、これらの廃止されたCPUでは分岐予測が機能しません。
bt
(ビットテスト)レジスタargは高速です。 1をerrNumber
ビットだけ左にシフトしてtest
を実行する作業を組み合わせていますが、1サイクルのレイテンシであり、1つのIntel uopのみです。 CISCのセマンティクスが原因でメモリ引数が遅い:「ビット文字列」のメモリオペランドを使用すると、テスト対象のバイトのアドレスが他の引数(8で除算)に基づいて計算され、isn 'メモリオペランドが指す1、2、4、または8バイトのチャンクに制限されません。
Agner Fogの命令テーブル から、可変カウントのシフト命令は、最近のIntelのbt
よりも遅くなります(1ではなく2 uops、shiftは必要なすべてを行いません)。
IMOこれは、フォールスルーが行われたスイッチの完璧な例です。
while (true) != while (loop)
おそらく最初のループはコンパイラーによって最適化されているため、ループカウントを増やすと2番目のループが遅くなる理由を説明できます。
審美的に私はこのアプローチを好む傾向があります。
unsigned int special_events[] = {
ERROR_01,
ERROR_07,
ERROR_0A,
ERROR_10,
ERROR_15,
ERROR_16,
ERROR_20
};
int special_events_length = sizeof (special_events) / sizeof (unsigned int);
void process_event(unsigned int numError) {
for (int i = 0; i < special_events_length; i++) {
if (numError == special_events[i]) {
fire_special_event();
break;
}
}
}
データを少し賢くして、ロジックを少し暗くすることができます。
私はそれが奇妙に見えることに気付きます。ここにインスピレーションがあります(私がPythonでそれをする方法から):
special_events = [
ERROR_01,
ERROR_07,
ERROR_0A,
ERROR_10,
ERROR_15,
ERROR_16,
ERROR_20,
]
def process_event(numError):
if numError in special_events:
fire_special_event()
ベストプラクティスについてはわかりませんが、スイッチを使用します-そして、「デフォルト」を介して意図的なフォールスルーをトラップします
ケースが将来的にグループ化されたままになる可能性が高い場合(複数のケースが1つの結果に対応する場合)、スイッチの方が読みやすく、保守しやすいことがわかります。
それらは同様に機能します。最新のコンパイラを使用した場合、パフォーマンスはほぼ同じです。
Ifステートメントは、より読みやすく、柔軟性が高いため、caseステートメントよりも好きです。「|| max <min」など、数値の等価性に基づかない他の条件を追加できます。ただし、ここに投稿した簡単なケースでは、それは実際には問題ではなく、最も読みやすいものを実行するだけです。
スイッチが確実に優先されます。長いif条件を読むよりも、スイッチのケースのリストを見て、それが何をしているのかを確実に知る方が簡単です。
if
状態での複製は目に難しいです。 ==
の1つが!=
と書かれているとします。気づきますか?または、「numError」のインスタンスの1つが「nmuError」と書き込まれた場合、それはたまたまコンパイルされましたか?
私は通常、スイッチの代わりにポリモーフィズムを使用することを好みますが、コンテキストの詳細がなければ、言うのは困難です。
パフォーマンスに関しては、プロファイラーを使用して、実際に予想される条件と同様の条件でアプリケーションのパフォーマンスを測定することをお勧めします。そうでなければ、おそらく間違った場所で間違った方法で最適化を行っています。
SWITCHを使用すると言います。この方法では、異なる結果を実装するだけで済みます。 10個の同一のケースでデフォルトを使用できます。必要な変更が1つだけあれば、その変更を明示的に実装するだけで、デフォルトを編集する必要はありません。また、IFやELSEIFを編集するよりも、SWITCHにケースを追加または削除する方がはるかに簡単です。
switch(numerror){
ERROR_20 : { fire_special_event(); } break;
default : { null; } break;
}
おそらくあなたの状態(この場合はnumerror)を可能性のリスト、おそらく配列に対してテストするかもしれません。
30個のエラーコードしかないので、独自のジャンプテーブルをコーディングしてから、コンパイラが正しいことをすることを期待するのではなく、最適化の選択をすべて自分で行います(ジャンプは常に最も速くなります)。また、コードが非常に小さくなります(ジャンプテーブルの静的宣言を除く)。また、デバッガーを使用すると、テーブルデータを直接確認するだけで、必要に応じて実行時に動作を変更できるという副次的な利点もあります。
プログラムのコンパイルに関しては、違いがあるかどうかはわかりません。しかし、プログラム自体とコードをできるだけシンプルに保つことに関しては、あなたが何をしたいかに依存していると個人的に考えています。 if else if elseステートメントには利点があります。
関数(標準ライブラリまたは個人)を条件として使用できる特定の範囲に対して変数をテストできます。
(例:
`int a;
cout<<"enter value:\n";
cin>>a;
if( a > 0 && a < 5)
{
cout<<"a is between 0, 5\n";
}else if(a > 5 && a < 10)
cout<<"a is between 5,10\n";
}else{
"a is not an integer, or is not in range 0,10\n";
ただし、If else if elseステートメントは(あなたの最善の試みにもかかわらず)急いで複雑で面倒になります。 Switchステートメントは、より明確で、クリーンで、読みやすい傾向があります。ただし、特定の値に対するテストにのみ使用できます(例:
`int a;
cout<<"enter value:\n";
cin>>a;
switch(a)
{
case 0:
case 1:
case 2:
case 3:
case 4:
case 5:
cout<<"a is between 0,5 and equals: "<<a<<"\n";
break;
//other case statements
default:
cout<<"a is not between the range or is not a good value\n"
break;
If-else if-elseステートメントが好きですが、それはあなた次第です。関数を条件として使用する場合、または範囲、配列、ベクトルに対して何かをテストする場合、および/または複雑なネストを処理することを気にしない場合は、If else if elseブロックを使用することをお勧めします。単一の値に対してテストする場合、またはクリーンで読みやすいブロックが必要な場合は、switch()caseブロックを使用することをお勧めします。
私はその古いことを知っていますが
public class SwitchTest {
static final int max = 100000;
public static void main(String[] args) {
int counter1 = 0;
long start1 = 0l;
long total1 = 0l;
int counter2 = 0;
long start2 = 0l;
long total2 = 0l;
boolean loop = true;
start1 = System.currentTimeMillis();
while (true) {
if (counter1 == max) {
break;
} else {
counter1++;
}
}
total1 = System.currentTimeMillis() - start1;
start2 = System.currentTimeMillis();
while (loop) {
switch (counter2) {
case max:
loop = false;
break;
default:
counter2++;
}
}
total2 = System.currentTimeMillis() - start2;
System.out.println("While if/else: " + total1 + "ms");
System.out.println("Switch: " + total2 + "ms");
System.out.println("Max Loops: " + max);
System.exit(0);
}
}
ループカウントを変えると大きく変わります。
While if/else:5msスイッチ:1ms最大ループ:100000
While if/else:5msスイッチ:3ms最大ループ:1000000
While if/else:5msスイッチ:14ms最大ループ:10000000
While if/else:5msスイッチ:149ms Max Loops:100000000
(必要に応じてステートメントを追加してください)
明確性と慣習のためにifステートメントを選択しますが、一部の人は同意しないと確信しています。結局、あなたは何かをしたいif
いくつかの条件が真です! 1つのアクションでスイッチを設定することは少し...不必要に思えます。
スイッチを使用してください。 ifステートメントには、条件の数に比例した時間がかかります。
私は速度とメモリ使用量についてあなたに話す人ではありませんが、スイッチの文を見ると、大きなif文(特に2-3ヶ月後)の理解がはるかに簡単です