例外がスローされない場合、try / catchブロックはパフォーマンスを低下させますか?


274

マイクロソフトの従業員とのコードレビュー中に、try{}ブロック内のコードの大きなセクションに遭遇しました。彼女とIT担当者は、これがコードのパフォーマンスに影響を与える可能性があることを示唆しました。実際、彼らはほとんどのコードはtry / catchブロックの外にあるべきであり、重要なセクションだけがチェックされるべきであると提案しました。マイクロソフト社の従業員は、次のホワイトペーパーを追加して、不正なtry / catchブロックに対して警告を発すると述べています。

調べてみたところ、最適化影響する可能性があることわかりました、スコープ間で変数が共有されている場合にのみ適用されるようです。

私はコードの保守性について質問したり、適切な例外を処理したりすることもしていません(問題のコードは間違いなくリファクタリングが必要です)。フロー制御の例外の使用についても言及していません。これはほとんどの場合明らかに間違っています。これらは重要な問題です(いくつかはより重要です)が、ここでは焦点を当てていません。

例外がスローされない場合、try / catchブロックはパフォーマンスにどのように影響しますか?


147
「パフォーマンスの正確さを犠牲にする彼はどちらにも値しない。」
Joel Coehoorn、2009

16
とはいえ、正確さは必ずしもパフォーマンスのために犠牲にされる必要はありません。
Dan Davies Brackett

19
単純な好奇心はどうですか?
サマンサブラナム

63
@ジョエル:たぶんコビは好奇心から答えを知りたいだけなのかもしれません。パフォーマンスが向上するか低下するかを知っているからといって、必ずしも彼が自分のコードでクレイジーなことをするつもりはありません。それ自体のための知識の追求は良いことではないですか?
LukeH 2009

6
この変更を行うかどうかを知るための良いアルゴリズムを以下に示します。最初に、有意義な顧客ベースのパフォーマンス目標を設定します。次に、最初に正しくて明確になるようにコードを記述します。第三に、あなたの目標に対してそれをテストします。第4に、目標を達成したら、早めに仕事を辞めて、ビーチに行きます。5番目に、目標を達成できない場合は、プロファイラーを使用して、遅すぎるコードを見つけます。6番目に、不必要な例外ハンドラーが原因でコードが遅くなりすぎた場合は、例外ハンドラーを削除してください。そうでない場合は、実際には遅すぎるコードを修正します。次に、ステップ3に戻ります。
Eric Lippert、

回答:


203

確認してください。

static public void Main(string[] args)
{
    Stopwatch w = new Stopwatch();
    double d = 0;

    w.Start();

    for (int i = 0; i < 10000000; i++)
    {
        try
        {
            d = Math.Sin(1);
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex.ToString());
        }
    }

    w.Stop();
    Console.WriteLine(w.Elapsed);
    w.Reset();
    w.Start();

    for (int i = 0; i < 10000000; i++)
    {
        d = Math.Sin(1);
    }

    w.Stop();
    Console.WriteLine(w.Elapsed);
}

出力:

00:00:00.4269033  // with try/catch
00:00:00.4260383  // without.

ミリ秒単位:

449
416

新しいコード:

for (int j = 0; j < 10; j++)
{
    Stopwatch w = new Stopwatch();
    double d = 0;
    w.Start();

    for (int i = 0; i < 10000000; i++)
    {
        try
        {
            d = Math.Sin(d);
        }

        catch (Exception ex)
        {
            Console.WriteLine(ex.ToString());
        }

        finally
        {
            d = Math.Sin(d);
        }
    }

    w.Stop();
    Console.Write("   try/catch/finally: ");
    Console.WriteLine(w.ElapsedMilliseconds);
    w.Reset();
    d = 0;
    w.Start();

    for (int i = 0; i < 10000000; i++)
    {
        d = Math.Sin(d);
        d = Math.Sin(d);
    }

    w.Stop();
    Console.Write("No try/catch/finally: ");
    Console.WriteLine(w.ElapsedMilliseconds);
    Console.WriteLine();
}

新しい結果:

   try/catch/finally: 382
No try/catch/finally: 332

   try/catch/finally: 375
No try/catch/finally: 332

   try/catch/finally: 376
No try/catch/finally: 333

   try/catch/finally: 375
No try/catch/finally: 330

   try/catch/finally: 373
No try/catch/finally: 329

   try/catch/finally: 373
No try/catch/finally: 330

   try/catch/finally: 373
No try/catch/finally: 352

   try/catch/finally: 374
No try/catch/finally: 331

   try/catch/finally: 380
No try/catch/finally: 329

   try/catch/finally: 374
No try/catch/finally: 334

24
JITコンパイルが前者に影響を与えていないことを確認するために、それらを逆の順序で試すこともできますか?
JoshJordan 2009

28
このようなプログラムは、例外処理の影響をテストするのに適した候補とは思えません。通常のtry {} catch {}ブロックで行われていることの多くは、最適化されます。私はそれで昼食に出かけるかもしれません...
LorenVS

30
これはデバッグビルドです。JITはそれらを最適化しません。
ベンM

7
これはまったく真実ではありません。考えてみてください。何回使用してループでキャッチを試しますか?ほとんどの場合、try.cでループを使用します
Athiwat Chunlakhan 2009

9
本当に?「例外がスローされない場合、try / catchブロックはパフォーマンスにどのように影響しますか?」
ベンM

105

try / catchを持つためとのtry / catchせずに、すべての統計情報を見た後、好奇心を見て私を強制的に後ろの両方のケースのために生成されるかを確認します。これがコードです:

C#:

private static void TestWithoutTryCatch(){
    Console.WriteLine("SIN(1) = {0} - No Try/Catch", Math.Sin(1)); 
}

MSIL:

.method private hidebysig static void  TestWithoutTryCatch() cil managed
{
  // Code size       32 (0x20)
  .maxstack  8
  IL_0000:  nop
  IL_0001:  ldstr      "SIN(1) = {0} - No Try/Catch"
  IL_0006:  ldc.r8     1.
  IL_000f:  call       float64 [mscorlib]System.Math::Sin(float64)
  IL_0014:  box        [mscorlib]System.Double
  IL_0019:  call       void [mscorlib]System.Console::WriteLine(string,
                                                                object)
  IL_001e:  nop
  IL_001f:  ret
} // end of method Program::TestWithoutTryCatch

C#:

private static void TestWithTryCatch(){
    try{
        Console.WriteLine("SIN(1) = {0}", Math.Sin(1)); 
    }
    catch (Exception ex){
        Console.WriteLine(ex);
    }
}

MSIL:

.method private hidebysig static void  TestWithTryCatch() cil managed
{
  // Code size       49 (0x31)
  .maxstack  2
  .locals init ([0] class [mscorlib]System.Exception ex)
  IL_0000:  nop
  .try
  {
    IL_0001:  nop
    IL_0002:  ldstr      "SIN(1) = {0}"
    IL_0007:  ldc.r8     1.
    IL_0010:  call       float64 [mscorlib]System.Math::Sin(float64)
    IL_0015:  box        [mscorlib]System.Double
    IL_001a:  call       void [mscorlib]System.Console::WriteLine(string,
                                                                  object)
    IL_001f:  nop
    IL_0020:  nop
    IL_0021:  leave.s    IL_002f //JUMP IF NO EXCEPTION
  }  // end .try
  catch [mscorlib]System.Exception 
  {
    IL_0023:  stloc.0
    IL_0024:  nop
    IL_0025:  ldloc.0
    IL_0026:  call       void [mscorlib]System.Console::WriteLine(object)
    IL_002b:  nop
    IL_002c:  nop
    IL_002d:  leave.s    IL_002f
  }  // end handler
  IL_002f:  nop
  IL_0030:  ret
} // end of method Program::TestWithTryCatch

私はILの専門家ではありませんが、ローカル例外オブジェクトが4行目に作成され、.locals init ([0] class [mscorlib]System.Exception ex)その後17行目まではtry / catchを使用しないメソッドの場合とほぼ同じIL_0021: leave.s IL_002fです。例外が発生すると、コントロールは行IL_0025: ldloc.0にジャンプしIL_002d: leave.s IL_002fます。それ以外の場合は、ラベルにジャンプし、関数が戻ります。

例外が発生しない場合は、例外オブジェクトとジャンプ命令のみを保持するのがローカル変数の作成のオーバーヘッドであると、私は安全に想定できます。


33
まあ、ILにはC#と同じ表記でtry / catchブロックが含まれているため、実際には、try / catchが舞台裏でどれほどのオーバーヘッドを意味するかはわかりません!ILがそれ以上追加しないことは、コンパイルされたアセンブリコードに何かが追加されないのと同じことではありません。ILは、すべての.NET言語の単なる共通表現です。マシンコードではありません!
畏敬の念

64

いいえ。try/ finallyブロックが取るに足らない最適化によって実際にプログラムに測定可能な影響がある場合は、最初から.NETを使用しないでください。


10
それは素晴らしい点です-私たちのリストの他のアイテムと比較すると、これは非常に小さいはずです。基本的な言語機能が正しく動作し、制御できるもの(SQL、インデックス、アルゴリズム)を最適化することを信頼する必要があります。
コビ

3
タイトなループメイトと考えてください。ゲームサーバーのソケットデータストリームからオブジェクトを読み取って逆シリアル化するループと、できる限りスクイーズしようとするループの例。したがって、バイナリフォーマッタではなくオブジェクトシリアル化用のMessagePackを使用し、バイト配列などを作成する代わりにArrayPool <byte>を使用します。これらのシナリオでは、複数の(おそらくネストされている)の影響は、タイトループ内でブロックをキャッチしてみます。一部の最適化はコンパイラによってスキップされ、例外変数はGen0 GCに渡されます。私が言っているのは、すべてが影響を与える「いくつかの」シナリオがあるということです。
tcwicks

35

.NET例外モデルのかなり包括的な説明。

リコマリアーニのパフォーマンスのヒント:例外コスト:スローするタイミングとしないタイミング

最初の種類のコストは、コードで例外処理を行う静的コストです。管理された例外は実際にはここで比較的うまく機能します。つまり、静的コストはC ++で言うよりもはるかに低くなる可能性があります。どうしてこれなの?静的コストは、実際には2種類の場所で発生します。1つ目は、try / finally / catch / throwの実際のサイトで、これらの構成要素のコードがある場所です。次に、アンマネージコードでは、例外がスローされた場合に破棄する必要があるすべてのオブジェクトを追跡することに関連するステルスコストがあります。存在しなければならないかなりの量のクリーンアップロジックがあり、卑劣な部分は、

ドミトリ・ザスラフスキー:

Chris Brummeのメモによると:キャッチが存在する場合、JITによって一部の最適化が実行されないという事実に関連するコストもあります。


1
C ++に関することは、標準ライブラリの非常に大きなチャンクが例外をスローすることです。それらについてのオプションは何もありません。ある種の例外ポリシーを使用してオブジェクトを設計する必要があります。そうすると、ステルスコストはなくなります。
デビッドソーンリー

Rico Marianiの主張は、ネイティブC ++に対して完全に間違っています。「静的コストはC ++で言うよりもはるかに低くなる可能性があります」-これは単に真実ではありません。しかし、記事が書かれたときの2003年の例外メカニズムの設計が何であったかはわかりません。例外がスローされない場合は、try / catchブロックの数と場所に関係なく、C ++にはまったくコストがかかりません
BJovke 2017年

1
@BJovke C ++の「ゼロコストの例外処理」は、例外がスローされない場合のランタイムコストがないことを意味しますが、すべてのクリーンアップコードが例外のデストラクタを呼び出すため、コードサイズに大きなコストがかかります。また、通常のコードパスで生成された例外固有のコードはありませんが、例外の可能性がオプティマイザを制限するため、コストは実際にはゼロではありません(たとえば、例外が必要な場合に必要なものはとどまる必要があります)どこかで->値があまり積極的に破棄されない->レジスタ割り当ての効率が低下する)
Daniel

24

この例では、構造がBen Mと異なります。これは、内部forループ内のオーバーヘッドが拡張されるため、2つのケースを適切に比較できなくなります。

以下は、チェックするコード全体(変数の宣言を含む)がTry / Catchブロック内にある比較の方が正確です。

        for (int j = 0; j < 10; j++)
        {
            Stopwatch w = new Stopwatch();
            w.Start();
            try { 
                double d1 = 0; 
                for (int i = 0; i < 10000000; i++) { 
                    d1 = Math.Sin(d1);
                    d1 = Math.Sin(d1); 
                } 
            }
            catch (Exception ex) {
                Console.WriteLine(ex.ToString()); 
            }
            finally { 
                //d1 = Math.Sin(d1); 
            }
            w.Stop(); 
            Console.Write("   try/catch/finally: "); 
            Console.WriteLine(w.ElapsedMilliseconds); 
            w.Reset(); 
            w.Start(); 
            double d2 = 0; 
            for (int i = 0; i < 10000000; i++) { 
                d2 = Math.Sin(d2);
                d2 = Math.Sin(d2); 
            } 
            w.Stop(); 
            Console.Write("No try/catch/finally: "); 
            Console.WriteLine(w.ElapsedMilliseconds); 
            Console.WriteLine();
        }

Ben Mの元のテストコードを実行したところ、Debug構成とReleas構成の両方に違いがあることに気付きました。

このバージョンでは、デバッグバージョンの違い(実際には他のバージョンよりも多い)に気づきましたが、リリースバージョンの違いはありませんでした。

結論
これらのテストに基づくと、Try / Catchパフォーマンスにわずかな影響を与えると言えます。

編集:
ループ値を10000000から1000000000に増やして、リリースで再度実行してリリースにいくつかの違いを付けようとしましたが、結果は次のとおりでした:

   try/catch/finally: 509
No try/catch/finally: 486

   try/catch/finally: 479
No try/catch/finally: 511

   try/catch/finally: 475
No try/catch/finally: 477

   try/catch/finally: 477
No try/catch/finally: 475

   try/catch/finally: 475
No try/catch/finally: 476

   try/catch/finally: 477
No try/catch/finally: 474

   try/catch/finally: 475
No try/catch/finally: 475

   try/catch/finally: 476
No try/catch/finally: 476

   try/catch/finally: 475
No try/catch/finally: 476

   try/catch/finally: 475
No try/catch/finally: 474

結果は重要ではないことがわかります。場合によっては、実際にはTry / Catchを使用したバージョンの方が高速です。


1
私もこれに気づきました。try/ catchを使用するとより高速になる場合があります。私はそれをベンの答えにコメントしました。ただし、24票の有権者とは異なり、この種のベンチマークは好きではありません。良い指標とは思いません。この場合、コードはより高速ですが、常に高速になりますか?
コビ

5
これは、マシンが他のさまざまなタスクを同時に実行していたことを証明していませんか?経過時間は決して適切な尺度ではありません。経過時間ではなく、プロセッサー時間を記録するプロファイラーを使用する必要があります。
コリンデズモンド、

2
@Kobi:プログラムが他のものよりも高速に実行されていることを証明するものとして公開する場合、これがベンチマークの最良の方法ではないことに同意しますが、開発者はある方法が他の方法よりも優れていることを示すことができます。この場合、(少なくともリリース構成では)違いは無視できると言えます。
畏敬の念を示す、

1
try/catchここでタイミングを計っていません。あなたは、10Mループに対して12 try / catch クリティカルセクションに入るタイミングを計っています。ループのノイズは、try / catchの影響を排除します。代わりに、タイトなループ内にtry / catchを配置して、比較して、または比較しない場合、try / catchのコストが発生します。(間違いなく、そのようなコーディングは一般的には良い習慣ではありませんが、構成のオーバーヘッドのタイミングを計りたい場合は、そのようにします)。現在、BenchmarkDotNetは信頼性の高い実行タイミングのための頼りになるツールです。
アベル

15

try..catchタイトループでaの実際の影響をテストしましたが、それ自体は小さすぎて、通常の状況ではパフォーマンスの問題にはなりません。

ループがほとんど機能しない場合(私のテストではを実行しましたx++)、例外処理の影響を測定できます。例外処理を含むループの実行には、約10倍の時間がかかりました。

ループが実際の作業を行う場合(私のテストでは、Int32.Parseメソッドを呼び出しました)、例外処理の影響が小さすぎて測定できません。ループの順序を入れ替えることで、はるかに大きな違いが生まれました...


11

catchブロックを試してもパフォーマンスへの影響はごくわずかですが、例外のスローはかなり大きくなる可能性があります。これはおそらく同僚が混乱した場所です。


8

try / catchはパフォーマンスに影響します。

しかし、それは大きな影響ではありません。try / catchの複雑さは、ループに配置される場合を除いて、単純な割り当てと同じように、通常O(1)です。したがって、それらを賢く使用する必要があります。

以下は、try / catchのパフォーマンスに関するリファレンスです(ただし、その複雑さについては説明していませんが、暗示されています)。「例外のスローを減らす」セクションをご覧ください


3
複雑さはO(1)ですが、あまり意味があるわけではありません。たとえば、try-catchで頻繁に呼び出されるコードセクションを装備している場合(またはループについて言及している場合)、O(1)は最終的には測定可能な数になる可能性があります。
Csaba Toth 2013

6

理論的には、実際に例外が発生しない限り、try / catchブロックはコードの動作に影響を与えません。ただし、try / catchブロックの存在が大きな影響を与える可能性のあるまれな状況と、その影響が顕著になる可能性のある、あまり一般的ではないがほとんど不明瞭な状況があります。この理由は、次のようなコードが指定されているためです。

Action q;
double thing1()
  { double total; for (int i=0; i<1000000; i++) total+=1.0/i; return total;}
double thing2()
  { q=null; return 1.0;}
...
x=thing1();     // statement1
x=thing2(x);    // statement2
doSomething(x); // statement3

コンパイラーは、statement2がstatement3の前に実行されることが保証されているという事実に基づいて、statement1を最適化できる場合があります。コンパイラがthing1に副作用がなく、thing2が実際にxを使用していないことを認識できる場合は、thing1を完全に省略してもかまいません。[この場合のように] thing1が高価な場合、それは大きな最適化になる可能性がありますが、thing1が高価な場合も、コンパイラーが最適化する可能性が最も低いケースです。コードが変更されたとしましょう:

x=thing1();      // statement1
try
{ x=thing2(x); } // statement2
catch { q(); }
doSomething(x);  // statement3

これで、statement2を実行せずにstatement3を実行できる一連のイベントが存在します。のコードでthing2例外がスローされない場合でも、別のスレッドがを使用しInterlocked.CompareExchangeqクリアされたことを通知しThread.ResetAbort、それをに設定してから、Thread.Abort()beforeステートメント2を実行してその値をに書き込む可能性がありますx。そして、catch実行でしょうThread.ResetAbort()[デリゲート経由でq実行がstatement3を続行することができ、]。もちろん、そのような一連のイベントは例外的にありそうもありませんが、そのようなありそうもないイベントが発生した場合でも、仕様に従って機能するコードを生成するにはコンパイラーが必要です。

一般に、コンパイラーは複雑なコードよりも単純なコードを除外する機会に気づく可能性が高いため、例外がスローされない場合、try / catchがパフォーマンスに大きな影響を与えることはまれです。それでも、try / catchブロックの存在によって最適化が妨げられる場合があります。ただし、try / catchの場合はコードの実行が高速化されていました。


5

処理よりも防止の方が優れています」、パフォーマンスと効率の観点から、事前変動よりも試行キャッチを選択することができました。以下のコードを考えてみましょう:

Stopwatch stopwatch = new Stopwatch();
stopwatch.Start();
for (int i = 1; i < int.MaxValue; i++)
{
    if (i != 0)
    {
        int k = 10 / i;
    }
}
stopwatch.Stop();
Console.WriteLine($"With Checking: {stopwatch.ElapsedMilliseconds}");
stopwatch.Reset();
stopwatch.Start();
for (int i = 1; i < int.MaxValue; i++)
{
    try
    {
        int k = 10 / i;
    }
    catch (Exception)
    {

    }
}
stopwatch.Stop();
Console.WriteLine($"With Exception: {stopwatch.ElapsedMilliseconds}");

結果は次のとおりです。

With Checking: 20367
With Exception: 13998

4

例外が発生しない場合に、try / catchブロックがどのように機能するか、および一部の実装でオーバーヘッドが高く、一部でゼロのオーバーヘッドがある実装の説明については、try / catch実装に関する説明を参照してください。特に、Windows 32ビットの実装には高いオーバーヘッドがあり、64ビットの実装にはないオーバーヘッドがあると思います。


私が説明したのは、例外を実装するための2つの異なるアプローチです。これらのアプローチは、C ++とC#、およびマネージ/アンマネージコードに等しく適用されます。MSがC#のために選択したものは正確にはわかりませんが、MSによって提供されるマシンレベルアプリケーションの例外処理アーキテクチャは、より高速なスキームを使用します。64ビット用のC#実装がそれを使用していなかったとしたら、少し驚きます。
Ira Baxter
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.