if-else ステートメントを切り替える利点
-
01-07-2019 - |
質問
を使用するためのベストプラクティスは何ですか? 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 してみて、switch を使用した同じコードと比較して、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 ;
}
}
コンパイルされません。
ほとんどの人は定義を使用し (ああ!)、同じコンパイル単位で定数変数を宣言および定義する人もいます。例えば:
// 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 宣言、または enum として定義すると、値がインライン化されるという副作用があります。したがって、定義、列挙値、または const int 値が変更された場合は、再コンパイルが必要になります。extern 宣言は、値が変更された場合に再コンパイルする必要がないことを意味しますが、その一方で switch を使用できなくなります。結論としては、 switch を使用すると、switch コードとケースとして使用される変数の間の結合が増加します。. 。OK になったら、スイッチを使用します。そうでない場合でも、驚くには当たりません。
.
2013 年 1 月 15 日編集:
ヴラド・ラザレンコ 私の回答にコメントし、スイッチによって生成されたアセンブリ コードの詳細な研究へのリンクを示しました。とても啓発的です: http://741mhz.com/switch/
いずれにしてもコンパイラはそれを最適化します。最も読みやすいスイッチを選択してください。
読みやすさだけを考えれば、Switch。私の意見では、巨大な if ステートメントは維持するのが難しく、読みにくいです。
エラー_01 :// 意図的なフォールスルー
または
(ERROR_01 == 数値エラー) ||
後者はエラーが発生しやすく、前者よりも多くの入力と書式設定が必要になります。
読みやすさを重視したコード。最適化とコンパイラはさまざまであり、パフォーマンスの問題が人々が考えている場所にあることはほとんどないため、何がよりパフォーマンスが高いかを知りたい場合は、プロファイラーを使用してください。
switch を使用してください。これがその目的であり、プログラマが期待しているものです。
ただし、私は冗長な大文字と小文字のラベルを入れます。人々に安心してもらうために、いつ、どのようなルールで大文字を省略するかを思い出そうとしていました。
次のプログラマーが言語の詳細について不必要に考えなくて済むようにする必要があります (数か月後にはあなたになるかもしれません!)。
コンパイラは最適化が非常に得意です switch
. 。最近の gcc は、一連の条件を最適化するのにも優れています。 if
.
いくつかのテストケースを作成しました ゴッドボルト.
とき 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
ビットマップに変換されますが、 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()
から 1 を減算する方法に注目してください。 errNumber
(と 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 (ブルドーザー以前) の利点のために、分岐ターゲットと次の条件分岐で使用されます。 「レプレット」とはどういう意味ですか?. 。これがないと、古い CPU では分岐予測がうまく機能しません。
bt
レジスタ引数を使用した (ビットテスト) は高速です。1 を左にシフトする作業を組み合わせます。 errNumber
ビットと実行中 test
, 、ただし、依然として 1 サイクルのレイテンシと単一の Intel uop のみです。あまりにも CISC セマンティクスのため、メモリ引数を使用すると遅くなります。「ビット文字列」のメモリ オペランドを使用すると、テストされるバイトのアドレスは、他の引数 (8 で除算) に基づいて計算され、指定された 1、2、4、または 8 バイトのチャンクに限定されません。メモリオペランドによって。
から アグナー・フォグの命令表, 、可変数シフト命令は、 bt
最近の Intel (1 つではなく 2 つの uop、そしてシフトは必要なことをすべて実行しません)。
私の意見では、これはスイッチのフォールスルーが何のために作られたかを示す完璧な例です。
ケースが今後もグループ化されたままになる可能性がある場合 (複数のケースが 1 つの結果に対応する場合)、切り替えのほうが読みやすく保守しやすいことが判明する可能性があります。
それらは同様にうまく機能します。最新のコンパイラを使用した場合、パフォーマンスはほぼ同じです。
私が case ステートメントよりも if ステートメントを好んでいるのは、if ステートメントの方が読みやすく、柔軟性が高いためです。「 || max < min 」など、数値の等価性に基づいていない他の条件を追加できます。ただし、ここに投稿した単純なケースでは、それはあまり重要ではなく、自分にとって最も読みやすいことを実行するだけです。
スイッチの方が断然お勧めです。長い if 条件を読むよりも、スイッチのケースのリストを見て、それが何をしているのかを確実に知る方が簡単です。
の重複 if
目に負担がかかる状態です。そのうちの 1 つを仮定します。 ==
書かれた !=
;気づきますか?それとも、「numError」の 1 つのインスタンスが「nmuError」と書かれていて、それがたまたまコンパイルされただけでしょうか?
一般的には、スイッチの代わりにポリモーフィズムを使用することを好みますが、コンテキストの詳細がなければ、何とも言えません。
パフォーマンスに関しては、プロファイラーを使用して、実際の環境で期待されるものと同様の条件でアプリケーションのパフォーマンスを測定するのが最善の策です。そうでない場合は、おそらく間違った場所と間違った方法で最適化を行っていることになります。
スイッチ ソリューションのコンパクトさには同意しますが、IMO では スイッチを乗っ取る ここ。
スイッチの目的は次のとおりです。 違う 値に応じた扱いとなります。
アルゴリズムを疑似コードで説明する必要がある場合は、意味的に次のとおりであるため、if を使用します。 何かエラーが発生した場合はこれを実行してください...
したがって、いつか各エラーに特定のコードを含めるようにコードを変更するつもりがない限り、私は次のようにします。 もし.
ベストプラクティスについてはわかりませんが、スイッチを使用して、「デフォルト」を介して意図的なフォールスルーをトラップします。
美的観点から、私はこのアプローチを好む傾向があります。
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()
while (true) != while (loop)
おそらく最初のループはコンパイラによって最適化されており、ループ数を増やすと 2 番目のループが遅くなる理由がこれで説明されます。
同意しない人もいると思いますが、私は明確さと慣例のために if ステートメントを選択します。結局のところ、あなたは何かをしたいのです if
ある条件が真です!ワンアクションでスイッチがあるのはちょっと…。不必要です。
私は速度やメモリ使用量について語る人間ではありませんが、switch ステートメントを見ると、大きな if ステートメントよりもはるかに理解しやすいです (特に 2 ~ 3 か月後)
SWITCHを使うといいと思います。この方法では、異なる結果を実装するだけで済みます。10 個の同一のケースではデフォルトを使用できます。変更する場合は、その変更を明示的に実装するだけでよく、デフォルトを編集する必要はありません。また、IF や ELSEIF を編集するよりも、SWITCH にケースを追加または削除する方がはるかに簡単です。
switch(numerror){
ERROR_20 : { fire_special_event(); } break;
default : { null; } break;
}
おそらく、条件 (この場合は numerror) を可能性のリスト (配列) に対してテストして、結果が確実に得られない限り SWITCH が使用されないようにすることもできます。
エラー コードが 30 個しかないことを確認して、独自のジャンプ テーブルをコーディングし、コンパイラが正しい動作をすることを期待するのではなく、すべての最適化の選択を自分で行います (ジャンプが常に最も速くなります)。また、(ジャンプ テーブルの静的宣言を除けば) コードが非常に小さくなります。また、デバッガーを使用すると、必要に応じてテーブル データを直接読み取るだけで、実行時の動作を変更できるという副次的な利点もあります。
古いのはわかるけど、
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);
}
}
ループ数を変えると大きく変わります。
if/else の場合:5msスイッチ:1msの最大ループ:100000
if/else の場合:5msスイッチ:3ms最大ループ:1000000
if/else の場合:5msスイッチ:14ms最大ループ:10000000
if/else の場合:5msスイッチ:149ms最大ループ:100000000
(必要に応じてステートメントを追加します)
プログラムのコンパイルに関しては、違いがあるかどうかはわかりません。ただし、プログラム自体とコードをできるだけシンプルにすることに関しては、個人的には、何をしたいかによって決まると思います。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 ブロックを使用することをお勧めします。