27

私は、それぞれの場合に巨大なswitchステートメントとif-elseステートメントを含むコードを見ていて、すぐに最適化の衝動を感じました。優れた開発者として、私は常にいくつかの難しいタイミングの事実を把握するために着手し、3つのバリエーションから始めるべきです。

  1. 元のコードは次のようになります。

    public static bool SwitchIfElse(Key inKey, out char key, bool shift)
    {
        switch (inKey)
        {
           case Key.A: if (shift) { key = 'A'; } else { key = 'a'; } return true;
           case Key.B: if (shift) { key = 'B'; } else { key = 'b'; } return true;
           case Key.C: if (shift) { key = 'C'; } else { key = 'c'; } return true;
           ...
           case Key.Y: if (shift) { key = 'Y'; } else { key = 'y'; } return true;
           case Key.Z: if (shift) { key = 'Z'; } else { key = 'z'; } return true;
           ...
           //some more cases with special keys...
        }
        key = (char)0;
        return false;
    }
    
  2. 条件演算子を使用するように変換された2番目のバリアント:

    public static bool SwitchConditionalOperator(Key inKey, out char key, bool shift)
    {
        switch (inKey)
        {
           case Key.A: key = shift ? 'A' : 'a'; return true;
           case Key.B: key = shift ? 'B' : 'b'; return true;
           case Key.C: key = shift ? 'C' : 'c'; return true;
           ...
           case Key.Y: key = shift ? 'Y' : 'y'; return true;
           case Key.Z: key = shift ? 'Z' : 'z'; return true;
           ...
           //some more cases with special keys...
        }
        key = (char)0;
        return false;
    }
    
  3. キーと文字のペアが事前に入力された辞書を使用したひねり:

    public static bool DictionaryLookup(Key inKey, out char key, bool shift)
    {
        key = '\0';
        if (shift)
            return _upperKeys.TryGetValue(inKey, out key);
        else
            return _lowerKeys.TryGetValue(inKey, out key);
    }
    

注:2つのswitchステートメントの大文字と小文字はまったく同じであり、辞書の文字数は同じです。

1)と2)のパフォーマンスは多少似ており、3)は少し遅くなると予想していました。

ウォームアップのために10.000.000回の反復を2回実行してから時間を計った各メソッドについて、驚いたことに、次の結果が得られました。

  1. 呼び出しあたり0.0000166ミリ秒
  2. 呼び出しあたり0.0000779ミリ秒
  3. 呼び出しあたり0.0000413ミリ秒

どうすればいいの?条件演算子は、if-elseステートメントよりも4倍遅く、辞書検索よりもほぼ2倍遅くなります。ここで重要な何かが欠けていますか、それとも条件演算子は本質的に遅いですか?

アップデート1:私のテストハーネスについて一言。Visual Studio2010のリリースコンパイル済み.Net3.5プロジェクトで、上記の各バリアントに対して次の(擬似)コードを実行します。コードの最適化がオンになり、DEBUG/TRACE定数がオフになります。時間指定の実行を行う前に、ウォームアップのために測定中のメソッドを1回実行します。shiftrunメソッドは、trueとfalseの両方に設定され、入力キーの選択セットを使用して、メソッドを多数の反復で実行しました。

Run(method);
var stopwatch = Stopwatch.StartNew();
Run(method);
stopwatch.Stop();
var measure = stopwatch.ElapsedMilliseconds / iterations;

Runメソッドは次のようになります。

for (int i = 0; i < iterations / 4; i++)
{
    method(Key.Space, key, true);
    method(Key.A, key, true);
    method(Key.Space, key, false);
    method(Key.A, key, false);
}

アップデート2:さらに掘り下げて、1)と2)で生成されたILを調べたところ、メインのスイッチ構造は予想どおりに同じであることがわかりましたが、ケース本体にはわずかな違いがあります。これが私が見ているILです:

1)if / elseステートメント:

L_0167: ldarg.2 
L_0168: brfalse.s L_0170

L_016a: ldarg.1 
L_016b: ldc.i4.s 0x42
L_016d: stind.i2 
L_016e: br.s L_0174

L_0170: ldarg.1 
L_0171: ldc.i4.s 0x62
L_0173: stind.i2 

L_0174: ldc.i4.1 
L_0175: ret 

2)条件演算子:

L_0165: ldarg.1 
L_0166: ldarg.2 
L_0167: brtrue.s L_016d

L_0169: ldc.i4.s 0x62
L_016b: br.s L_016f

L_016d: ldc.i4.s 0x42
L_016f: stind.i2 

L_0170: ldc.i4.1 
L_0171: ret 

いくつかの観察:

  • 条件演算子は、 shifttrueに等しい場合は分岐し、if/elseshiftはfalseの場合に分岐します。
  • 1)は実際には2)よりもいくつかの命令にコンパイルされますshiftが、がtrueまたはfalseの場合に実行される命令の数は、2つで同じです。
  • 1)の命令順序は、常に1つのスタックスロットのみが占有され、2)は常に2つをロードするようになっています。

これらの観察結果のいずれかは、条件演算子のパフォーマンスが遅くなることを意味しますか?関係する他の副作用はありますか?

4

8 に答える 8

13

非常に奇妙なことに、おそらく.NETの最適化があなたのケースで裏目に出ています:

著者は三項式のいくつかのバージョンを逆アセンブルし、1 つの小さな違いを除いて、それらが if ステートメントと同一であることを発見しました。三項ステートメントは、部分式が true かどうかをテストする代わりに false であることをテストするように、予想とは反対の条件をテストするコードを生成することがあります。これにより、一部の命令が並べ替えられ、パフォーマンスが向上する場合があります。

http://dotnetperls.com/ternary

列挙値の ToString を考慮したい場合があります(特別な場合ではありません):

string keyValue = inKey.ToString();
return shift ? keyValue : keyValue.ToLower();

編集:
if-else メソッドと三項演算子を比較しました。1000000 サイクルでは、三項演算子は常に if-else メソッドと同じくらい高速です (上記のテキストをサポートする数ミリ秒速い場合もあります)。かかった時間の測定で何らかのエラーが発生したと思います。

于 2010-02-14T01:06:20.683 に答える
11

これをデバッグ ビルドまたはリリース ビルドでテストしているかどうかを知りたいです。デバッグ ビルドの場合、リリース モードを使用する (または手動でデバッグ モードを無効にしてコンパイラの最適化を有効にする) ときにコンパイラが追加する低レベルの最適化が不足しているため、違いが生じる可能性が非常に高くなります。

ただし、最適化を行った場合、三項演算子は if/else ステートメントと同じか、少し速くなりますが、辞書検索は最も遅くなります。これが私の結果です。1,000 万回のウォームアップの反復に続いて、それぞれについて 1,000 万回の時間を計ったものです。

デバッグモード

   If/Else: 00:00:00.7211259
   Ternary: 00:00:00.7923924
Dictionary: 00:00:02.3319567

リリースモード

   If/Else: 00:00:00.5217478
   Ternary: 00:00:00.5050474
Dictionary: 00:00:02.7389423

ここで、最適化が有効になる前は 3 項計算が if/else よりも遅く、その後は高速になったことに注目することは興味深いと思います。

編集:

もう少しテストした後、実用的な意味で、if/else と 3 項の間にほとんどまたはまったく違いはありません。3 値コードは IL を小さくしますが、互いにほとんど同じように機能します。リリース モード バイナリを使用した 12 の異なるテストでは、if/else と 3 値の結果は同じか、10,000,000 回の反復で数ミリ秒ずれていました。if/else の方がわずかに速い場合もあれば、3 項の方が速い場合もありましたが、実用上は同じように動作します。

一方、辞書のパフォーマンスは大幅に低下します。この種の最適化に関して言えば、コードが既に存在する場合、if/else と 3 項のどちらを選択するかで時間を無駄にすることはありません。ただし、現在辞書を実装している場合は、間違いなくリファクタリングしてより効率的なアプローチを使用し、パフォーマンスを約 400% 向上させます (とにかく、特定の関数について)。

于 2010-02-14T02:08:15.863 に答える
4

興味深いことに、私はここで小さなクラスを開発しましたIfElseTernaryTest。OK、コードは実際には「最適化」されていないか、良い例ではありませんが、それでも...議論のために:

public class IfElseTernaryTest
{
    private bool bigX;
    public void RunIfElse()
    {
        int x = 4; int y = 5;
        if (x &gt; y) bigX = false;
        else if (x &lt; y) bigX = true; 
    }
    public void RunTernary()
    {
        int x = 4; int y = 5;
        bigX = (x &gt; y) ? false : ((x &lt; y) ? true : false);
    }
}

これはコードの IL ダンプでした...興味深い部分は、IL の 3 進命令が実際にはif....

.class /*02000003*/ public auto ansi beforefieldinit ConTern.IfElseTernaryTest
       extends [mscorlib/*23000001*/]System.Object/*01000001*/
{
  .field /*04000001*/ private bool bigX
  .method /*06000003*/ public hidebysig instance void 
          RunIfElse() cil managed
  // SIG: 20 00 01
  {
    // Method begins at RVA 0x205c
    // Code size       44 (0x2c)
    .maxstack  2
    .locals /*11000001*/ init ([0] int32 x,
             [1] int32 y,
             [2] bool CS$4$0000)
    .line 19,19 : 9,10 ''
//000013:     }
//000014: 
//000015:     public class IfElseTernaryTest
//000016:     {
//000017:         private bool bigX;
//000018:         public void RunIfElse()
//000019:         {
    IL_0000:  /* 00   |                  */ nop
    .line 20,20 : 13,23 ''
//000020:             int x = 4; int y = 5;
    IL_0001:  /* 1A   |                  */ ldc.i4.4
    IL_0002:  /* 0A   |                  */ stloc.0
    .line 20,20 : 24,34 ''
    IL_0003:  /* 1B   |                  */ ldc.i4.5
    IL_0004:  /* 0B   |                  */ stloc.1
    .line 21,21 : 13,23 ''
//000021:             if (x &gt; y) bigX = false;
    IL_0005:  /* 06   |                  */ ldloc.0
    IL_0006:  /* 07   |                  */ ldloc.1
    IL_0007:  /* FE02 |                  */ cgt
    IL_0009:  /* 16   |                  */ ldc.i4.0
    IL_000a:  /* FE01 |                  */ ceq
    IL_000c:  /* 0C   |                  */ stloc.2
    IL_000d:  /* 08   |                  */ ldloc.2
    IL_000e:  /* 2D   | 09               */ brtrue.s   IL_0019

    .line 21,21 : 24,37 ''
    IL_0010:  /* 02   |                  */ ldarg.0
    IL_0011:  /* 16   |                  */ ldc.i4.0
    IL_0012:  /* 7D   | (04)000001       */ stfld      bool ConTern.IfElseTernaryTest/*02000003*/::bigX /* 04000001 */
    IL_0017:  /* 2B   | 12               */ br.s       IL_002b

    .line 22,22 : 18,28 ''
//000022:             else if (x &lt; y) bigX = true; 
    IL_0019:  /* 06   |                  */ ldloc.0
    IL_001a:  /* 07   |                  */ ldloc.1
    IL_001b:  /* FE04 |                  */ clt
    IL_001d:  /* 16   |                  */ ldc.i4.0
    IL_001e:  /* FE01 |                  */ ceq
    IL_0020:  /* 0C   |                  */ stloc.2
    IL_0021:  /* 08   |                  */ ldloc.2
    IL_0022:  /* 2D   | 07               */ brtrue.s   IL_002b

    .line 22,22 : 29,41 ''
    IL_0024:  /* 02   |                  */ ldarg.0
    IL_0025:  /* 17   |                  */ ldc.i4.1
    IL_0026:  /* 7D   | (04)000001       */ stfld      bool ConTern.IfElseTernaryTest/*02000003*/::bigX /* 04000001 */
    .line 23,23 : 9,10 ''
//000023:         }
    IL_002b:  /* 2A   |                  */ ret
  } // end of method IfElseTernaryTest::RunIfElse

  .method /*06000004*/ public hidebysig instance void 
          RunTernary() cil managed
  // SIG: 20 00 01
  {
    // Method begins at RVA 0x2094
    // Code size       27 (0x1b)
    .maxstack  3
    .locals /*11000002*/ init ([0] int32 x,
             [1] int32 y)
    .line 25,25 : 9,10 ''
//000024:         public void RunTernary()
//000025:         {
    IL_0000:  /* 00   |                  */ nop
    .line 26,26 : 13,23 ''
//000026:             int x = 4; int y = 5;
    IL_0001:  /* 1A   |                  */ ldc.i4.4
    IL_0002:  /* 0A   |                  */ stloc.0
    .line 26,26 : 24,34 ''
    IL_0003:  /* 1B   |                  */ ldc.i4.5
    IL_0004:  /* 0B   |                  */ stloc.1
    .line 27,27 : 13,63 ''
//000027:             bigX = (x &gt; y) ? false : ((x &lt; y) ? true : false);
    IL_0005:  /* 02   |                  */ ldarg.0
    IL_0006:  /* 06   |                  */ ldloc.0
    IL_0007:  /* 07   |                  */ ldloc.1
    IL_0008:  /* 30   | 0A               */ bgt.s      IL_0014

    IL_000a:  /* 06   |                  */ ldloc.0
    IL_000b:  /* 07   |                  */ ldloc.1
    IL_000c:  /* 32   | 03               */ blt.s      IL_0011

    IL_000e:  /* 16   |                  */ ldc.i4.0
    IL_000f:  /* 2B   | 01               */ br.s       IL_0012

    IL_0011:  /* 17   |                  */ ldc.i4.1
    IL_0012:  /* 2B   | 01               */ br.s       IL_0015

    IL_0014:  /* 16   |                  */ ldc.i4.0
    IL_0015:  /* 7D   | (04)000001       */ stfld      bool ConTern.IfElseTernaryTest/*02000003*/::bigX /* 04000001 */
    .line 28,28 : 9,10 ''
//000028:         }
    IL_001a:  /* 2A   |                  */ ret
  } // end of method IfElseTernaryTest::RunTernary

そのため、その三項演算子は明らかに短く、使用される命令が少ないほど高速だと思います...しかし、それに基づいて、驚くべきケース#2と矛盾しているようです...

編集: Sky のコメントの後、「#2 のコードの肥大化」を示唆していますが、これは Sky の発言を反証します!!! OK、コードが異なり、コンテキストが異なります。これは、IL ダンプをチェックして確認する演習の例です...

于 2010-02-14T01:40:57.360 に答える
3

#1と#2は同じだと思います。オプティマイザは同じコードになるはずです。#3 のディクショナリは、実際にハッシュを使用しないように何らかの方法で最適化されていない限り、低速であることが予想されます。

リアルタイムシステムをコーディングするときは、例に示されているように変換するために、常にルックアップテーブル(単純な配列)を使用しました。入力の範囲がかなり小さい場合は最速です。

于 2010-02-14T00:59:20.443 に答える
2

if ステートメントが辞書検索よりも遅いと期待する理由がよくわかりません。少なくともハッシュコードを計算する必要があり、リストで検索する必要があります。これが cmp/jmp よりも高速であると想定する理由がわかりません。

具体的には、あなたが最適化している方法がそれほど素晴らしいとは思いません。呼び出し段階で改善できるようです (ただし、コンテキストが提供されていないため、確信が持てません)。

于 2010-02-14T02:06:08.473 に答える
1

そのメソッドのパフォーマンスに関心があると仮定すると (そうでない場合は、わざわざ投稿する必要はありません)、値を配列に格納し、char値を配列のインデックスに変換することを検討する必要がありKeyます。

于 2010-02-14T18:44:24.317 に答える
0

手元にVSはありませんが、キーをキャラクターとして取得する簡単な組み込みの方法は確かにありますか? メソッドのようなもので、その怪物を次のtoStringように置き換えることができます。switch

if (shift)
  return inKey.toString().toUppercase();
else
  return inKey.toString().toLowercase();
于 2010-02-17T13:28:03.150 に答える
-1

私が 3 番目のオプションを選択するのは、それがより読みやすく、保守しやすいという理由だけです。このコードは、アプリケーションのパフォーマンスのボトルネックではないと確信しています。

于 2010-02-14T00:59:24.437 に答える