178

ステートメントswitchを使用する場合とステートメントを使用する場合のベスト プラクティスは何ですか? パフォーマンスとスペースを考慮する必要がありますが、重要ではありません。私はスニペットを抽象化したので、命名規則について私を嫌わないでください.ifunsigned

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();
}
4

23 に答える 23

174

スイッチを使用してください。

最悪の場合、コンパイラはif-elseチェーンと同じコードを生成するため、何も失うことはありません。疑わしい場合は、最も一般的なケースを最初にswitchステートメントに入れてください。

最良の場合、オプティマイザーはコードを生成するためのより良い方法を見つけるかもしれません。コンパイラが行う一般的なことは、バイナリ決定木を構築すること(平均的な場合は比較とジャンプを保存する)、または単にジャンプテーブルを構築すること(比較なしで機能する)です。

于 2008-09-18T23:32:46.613 に答える
45

あなたの例で提供した特別なケースでは、最も明確なコードはおそらく次のとおりです。

if (RequiresSpecialEvent(numError))
    fire_special_event();

明らかに、これは問題をコードの別の領域に移すだけですが、このテストを再利用する機会があります。また、それを解決するためのより多くのオプションがあります。たとえば、std::set を使用できます。

bool RequiresSpecialEvent(int numError)
{
    return specialSet.find(numError) != specialSet.end();
}

これが RequiresSpecialEvent の最適な実装であると言っているのではなく、オプションであるというだけです。スイッチや if-else チェーン、ルックアップ テーブル、値のビット操作などを引き続き使用できます。意思決定プロセスが不明瞭になればなるほど、それを独立した機能として持つことから得られる価値は大きくなります。

于 2008-09-24T20:01:19.853 に答える
25

スイッチより高速です。

ループ内で30の異なる値をif/elseしてみて、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 ;
    }
}

コンパイルされません。

ほとんどの人はdefine(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年9月21日編集:

bk1eは次のコメントを追加しました:「ヘッダーファイルの列挙型として定数を定義することは、これを処理する別の方法です」。

もちろん。

externタイプのポイントは、値をソースから切り離すことでした。この値をマクロとして、単純なconst int宣言として、または列挙型として定義すると、値をインライン化するという副作用があります。したがって、define、enum値、またはconst int値が変更された場合は、再コンパイルが必要になります。extern宣言は、値が変更された場合に再コンパイルする必要がないことを意味しますが、その一方で、switchを使用できなくなります。スイッチを使用するという結論は、スイッチコードとケースとして使用される変数の間の結合を増加させます。OKになったら、スイッチを使用します。そうでない場合は、当然のことです。

2013-01-15を編集:

Vlad Lazarenkoが私の答えにコメントし、スイッチによって生成されたアセンブリコードの彼の詳細な研究へのリンクを提供しました。非常に啓発的:http://lazarenko.me/switch/

于 2008-09-19T16:12:55.857 に答える
18

コンパイラはとにかくそれを最適化します-それが最も読みやすいのでスイッチに行きます。

于 2008-09-18T23:30:31.953 に答える
6

読みやすさのためのコード。何がより良いパフォーマンスを発揮するかを知りたい場合は、プロファイラーを使用してください。最適化とコンパイラーはさまざまであり、パフォーマンスの問題は、人々が思っている場所ではめったにありません。

于 2008-09-24T19:26:06.647 に答える
6

読みやすさのためだけの場合は、スイッチ。私の意見では、ステートメントを維持するのが難しく、読むのが難しい場合は巨大です。

ERROR_01://意図的なフォールスルー

また

(ERROR_01 == numError)||

後者はエラーが発生しやすく、前者よりも多くの入力とフォーマットが必要です。

于 2008-09-18T23:30:52.717 に答える
6

コンパイラは最適化が得意switchです。最近の gcc は、if.

godboltでいくつかのテストケースを作成しました。

case値が密接にグループ化されている場合、gcc、clang、および icc はすべて、ビットマップを使用して値が特別な値の 1 つであるかどうかを確認するのに十分なほどスマートです。

たとえば、gcc 5.2 -O3 はswitchto (および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<<errNumbermov/shift を使用して実行します。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()

errNumberleaその操作を移動と組み合わせるために)から1を引く方法に注意してください。movabsqこれにより、ビットマップを 32 ビットの即値に適合させ、より多くの命令バイトを必要とする 64 ビットの即値を回避できます。

より短い (マシン コードでの) シーケンスは次のようになります。

    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(ビットテスト)レジスタ引数を使用すると高速です。errNumber1 をビット単位で左シフトして を実行する作業を組み合わせますtestが、それでも 1 サイクルのレイテンシであり、Intel の uop は 1 つだけです。CISC セマンティクスが多すぎるため、メモリ引数を使用すると遅くなります。「ビット文字列」のメモリオペランドを使用すると、テストするバイトのアドレスは、他の引数 (8 で除算) に基づいて計算されます。メモリ オペランドが指す 1、2、4、または 8 バイトのチャンクに限定されません。

Agner Fog の命令テーブルから、可変カウント シフト命令はbt最近の Intel よりも低速です (1 uop ではなく 2 uop であり、シフトは必要な他のすべてを実行しません)。

于 2015-09-02T14:37:11.837 に答える
5

スイッチを使用してください。それが目的であり、プログラマーが期待するものです。

ただし、冗長なケース ラベルを入れます。人々が安心できるようにするために、いつ、どのようなルールでラベルを除外するかを思い出そうとしました。
それに取り組む次のプログラマーが、言語の詳細について不必要に考えなければならないようにしたくありません (数か月後にはあなたかもしれません!)。

于 2008-09-24T20:54:31.863 に答える
2

IMOこれは、スイッチのフォールスルーが何のために作られたかを示す完璧な例です。

于 2008-09-18T23:31:11.797 に答える
2

それらは等しくうまく機能します。最新のコンパイラを考えると、パフォーマンスはほぼ同じです。

読みやすく、柔軟性が高いため、caseステートメントよりもifステートメントの方が好きです。「|| max <min」のように、数値の同等性に基づかない他の条件を追加できます。しかし、ここに投稿した単純なケースの場合、それは実際には重要ではなく、最も読みやすいことを実行するだけです。

于 2008-09-18T23:33:49.467 に答える
1

審美的に私はこのアプローチを好む傾向があります。

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()
于 2008-09-24T20:46:11.027 に答える
1

ベストプラクティスについてはわかりませんが、スイッチを使用し、「デフォルト」を介して意図的なフォールスルーをトラップします

于 2008-09-18T23:30:56.523 に答える
1

ケースが将来グループ化されたままになる可能性がある場合(複数のケースが1つの結果に対応する場合)、スイッチは読みやすく、保守しやすいことがわかる場合があります。

于 2008-09-18T23:31:35.773 に答える
1

スイッチは間違いなく好まれます。long if条件を読み取るよりも、スイッチのケースのリストを調べて、スイッチが何をしているのかを確認する方が簡単です。

状態の重複ifは目には難しいです。==の1つが書かれたと仮定し!=ます; 気づきますか?または、「numError」の1つのインスタンスが「nmuError」と記述された場合、これはたまたまコンパイルされたのでしょうか。

私は通常、スイッチの代わりにポリモーフィズムを使用することを好みますが、コンテキストの詳細がなければ、言うのは難しいです。

パフォーマンスに関しては、プロファイラーを使用して、実際に期待する条件と同様の条件でアプリケーションのパフォーマンスを測定するのが最善の策です。そうでなければ、おそらく間違った場所で間違った方法で最適化しています。

于 2008-09-18T23:34:21.587 に答える
1

スイッチソリューションの互換性には同意しますが、IMOはここでスイッチを乗っ取っています.
スイッチの目的は、値に応じて異なる処理を行うことです。
疑似コードでアルゴを説明する必要がある場合は、if を使用します。これは、意味的には、それが何であるか
です。 、 ifを使用します。

于 2008-09-18T23:47:15.270 に答える
0

明確さと慣例のためにifステートメントを選択しますが、一部の人は同意しないと確信しています。結局のところ、あなたは何かif条件が本当である何かをしたいのです!1つのアクションでスイッチを使用するのは少し...不必要に思えます。

于 2008-09-18T23:31:30.330 に答える
0

私は速度とメモリ使用量についてあなたに話す人ではありませんが、スイッチステートメントを見るのは非常に理解しやすく、大きなifステートメント(特に2〜3ヶ月後)です

于 2008-09-18T23:31:48.003 に答える
0

SWITCHを使用すると思います。このようにして、異なる結果を実装するだけで済みます。10 個の同一のケースでデフォルトを使用できます。1 つの変更が必要な場合は、その変更を明示的に実装するだけでよく、デフォルトを編集する必要はありません。また、IF および ELSEIF を編集するよりも、SWITCH にケースを追加または削除する方がはるかに簡単です。

switch(numerror){
    ERROR_20 : { fire_special_event(); } break;
    default : { null; } break;
}

可能性のリスト、おそらく配列に対して条件(この場合はnumerror)をテストすることもできるので、間違いなく結果がない限り、SWITCHは使用されません。

于 2008-09-18T23:37:34.607 に答える
0

エラー コードが 30 個しかないので、独自のジャンプ テーブルをコーディングしてから、コンパイラが正しいことを行うことを期待するのではなく、すべての最適化の選択を自分で行います (ジャンプは常に最も高速です)。また、コードを非常に小さくします (ジャンプ テーブルの静的宣言を除く)。また、デバッガーを使用すると、テーブル データを直接突くだけで、必要に応じて実行時に動作を変更できるという副次的な利点もあります。

于 2008-09-19T13:18:15.497 に答える
0

古いのはわかるけど

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 最大ループ: 100000000

(必要に応じてステートメントを追加します)

于 2009-11-23T13:50:48.653 に答える
0

プログラムのコンパイルに関しては、違いがあるかどうかはわかりません。しかし、プログラム自体とコードをできるだけシンプルに保つことに関しては、個人的にはあなたが何をしたいかによると思います. 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() ケース ブロックを使用することをお勧めします。

于 2017-02-23T03:39:48.260 に答える