.NET例外はどのくらい遅いですか?


143

例外をスローするタイミングとスローしないタイミングについての議論はしたくありません。簡単な問題を解決したいと思います。例外をスローしないという主張は99%の確率でスローされますが、反対側は(ベンチマークテストを使用して)速度は問題ではないと主張します。私は、どちらか一方に関係する多数のブログ、記事、および投稿を読みました。どっち?

答えからのいくつかのリンク:スキートマリアーニBrumme


13
嘘、いまいましい嘘、ベンチマークがあります。:)
gbjbaanb 2008年

残念ながら、ここの投票数の多い回答のいくつかは、「例外はどれほど遅いのか?」実際に尋ねられた質問に対する1つの簡単な答えは、Windows CLRでは、例外は戻り値より750倍遅いです。
David Jeske 2016

回答:


207

私は「遅くない」側にいます-より正確には「通常の使用でそれらを避ける価値があるほど遅くない」のです。これについて2つの短い 記事を書きました。ベンチマークの側面への批判がありますが、ほとんどは「実際にはもっと多くのスタックを通過する必要があるため、キャッシュをブローするなど」ですが、エラーコードを使用してスタックを上に移動することもできます。キャッシュをブローするので、これは特に良い議論だとは思いません。

明確にするために-論理的でない例外の使用はサポートしていません。たとえばint.TryParse、ユーザーからのデータの変換には完全に適しています。機械で生成されたファイルを読み取る場合に適しています。失敗とは、「ファイルが本来の形式ではないため、他に何が問題なのかわからないので、これを処理したくない場合です。 」

「合理的な状況のみ」で例外を使用する場合、例外によってパフォーマンスが大幅に低下するアプリケーションを見たことがありません。基本的に、重大な正確性の問題がない限り、例外は頻繁に発生しないはずです。重大な正確性の問題がある場合、パフォーマンスは直面する最大の問題ではありません。


2
「例外的」な状況にある-あなたが言うように物事がなくなって間違っていたときに、残念ながら、人々は言われた例外は、無料で些細な「正しい」機能のためにそれらを使用し、それらは、使用されるべきである
gbjbaanbを

4
はい、例外を不適切に使用するとパフォーマンスコストが発生することに注意してください。彼ら適切に使用されている場合、それ問題ではないと思います:)
Jon Skeet

7
@PaulLockwood:1 あたり200以上の例外がある場合、例外を悪用していると思います。1秒あたり200回発生している場合、それは明らかに「例外的な」イベントではありません。回答の最後の文に注意してください。「基本的に、重大な正確性の問題がない限り例外は頻繁に発生しません。重大な正確性の問題がある場合、パフォーマンスは直面する最大の問題ではありません。」
Jon Skeet、

4
@PaulLockwood:私のポイントは、1秒あたり200以上の例外がある場合、それはおそらく例外を悪用していることをすでに示しているということです。よくあることですが、私は驚くことではありませんが、パフォーマンスの側面が私の最初の関心事ではないことを意味します-例外の乱用はそうなります。例外の不適切な使用をすべて削除したら、それらがパフォーマンスの重要な部分になるとは思わないでしょう。
Jon Skeet、

4
@DavidJeske:回答の要点を逃しました。明らかに、例外のスローは、通常の値を返すよりもはるかに遅いです。誰もそれを主張していません。問題は、それらが遅すぎるかどうかです。あなたがいる場合は、適切な例外をスローための状況パフォーマンスの問題を引き起こしているの、あなたはおそらく大きな問題を持っている-それは膨大な量があることを示唆しているので、間違ったシステムと。通常、問題は実際には例外が不適切である場合に例外を使用していることです。
Jon Skeet

31

それらを実装した人、Chris Brummeからの決定的な答えがあります。彼は件名について優れたブログ記事(警告-その非常に長い)(警告2-非常によく書かれている、あなたが技術者なら最後まで読んでから仕事の後の時間を補わなければならない:)を書いた。 )

要旨:彼らは遅い。それらはWin32 SEH例外として実装されているため、一部はリング0のCPU境界を通過します。明らかに現実の世界では、他の多くの作業を行うことになるので、奇妙な例外はまったく気付かれませんが、それらをプログラムフローに使用する場合は、アプリがハンマーで打たれることを期待してください。これは、MSマーケティングマシンが私たちに害を及ぼすもう1つの例です。オーバーヘッドがまったくゼロになり、完全に無事だった方法を教えてくれたマイクロソフト社を思い出します。

クリスは適切な引用をします:

実際、CLRは内部的に、エンジンの管理されていない部分でも例外を使用します。ただし、例外を伴う深刻な長期的なパフォーマンスの問題があり、これを決定に含める必要があります。


これは、null許容型が「これは通常のプログラムフローです」で何度も例外を発生させ、重大なパフォーマンスの問題を引き起こしている実際のテストで言及できます。Alwyasは、例外は例外的なケースのためのものであることを覚えておいてください。別の言い方をする人を信じないでください。
gbjbaanb

8

スローされたときだけ遅いと言っているとき、私は人々が何について話しているのかわかりません。

編集:例外がスローされない場合は、新しいException()などを実行していることを意味します。それ以外の場合は、例外によってスレッドが中断され、スタックがウォークされます。これは小さな状況では問題ないかもしれませんが、トラフィックの多いWebサイトでは、ワークフローまたは実行パスのメカニズムが例外であるため、パフォーマンスの問題が確実に発生します。例外自体は悪くないので、例外的な条件を表現するのに役立ちます

.NETアプリの例外ワークフローは、ファーストチャンスとセカンドチャンスの例外を使用します。すべての例外について、それらをキャッチして処理している場合でも、例外オブジェクトは引き続き作成され、フレームワークはスタックを走査してハンドラーを探す必要があります。もちろん、キャッチして再スローするのに時間がかかります-初回例外が発生し、それをキャッチして再スローすると、別の初回例外が発生し、ハンドラが見つかりません。セカンドチャンス例外。

例外はヒープ上のオブジェクトでもあるため、大量の例外をスローすると、パフォーマンスとメモリの両方の問題が発生します。

さらに、ACEチームが執筆した「Microsoft .NET Webアプリケーションのパフォーマンステスト」によると、

「例外処理にはコストがかかります。CLRが正しい例外ハンドラーを探してコールスタックを再帰している間、関連するスレッドの実行が中断されます。例外が見つかると、例外ハンドラーといくつかのfinallyブロックがすべて実行されるチャンスがなければなりません。通常の処理を行う前に」

この分野での私自身の経験から、例外を減らすことでパフォーマンスが大幅に向上することがわかりました。もちろん、パフォーマンステストの際に考慮すべき他の事項があります。たとえば、ディスクI / Oが実行された場合、またはクエリが数秒で実行された場合、それが焦点になります。ただし、例外を見つけて削除することは、その戦略の重要な部分です。


1
あなたが書いたものは、例外がスローされた場合にのみ例外が遅くなるという主張に矛盾しません。あなたは彼ら投げられる状況についてだけ話しました。例外を削除して「パフォーマンスを大幅に向上」させた場合:1)それらは真のエラー状態でしたか、それとも単なるユーザーエラーでしたか?
Jon Skeet、

2)デバッガーの下で実行されていましたか?
Jon Skeet、

スローしない場合に例外で実行できる唯一のことは、オブジェクトとして作成することです。これは意味がありません。デバッガの下にあるかどうかは問題ではありません-それでも遅くなります。はい、デバッガを接続すると発生するフックがありますが、それでも遅い
Cory Foy

4
私は知っています-私はMSFTのプレミアチームの一員でした。:)とにかく、たくさん-私たちが見たいくつかの極端なケースでは数千秒 ライブデバッガーに接続して、例外を読むのと同じくらいの速さで表示するようなものはありません。Exは遅い-DBへの接続も遅いので、理にかなっているときにそれを行う。
Cory Foy

5
Cory、「スローされたときだけ遅い」というポイントは、catch / finallyブロックが存在するだけなので、パフォーマンスについて心配する必要がないということです。つまり、これら自体がパフォーマンスに影響を与えることはなく、実際の例外インスタンスが発生するだけです。
Ian Horwill

6

私が理解している議論は、例外のスローが悪いということではなく、それ自体が遅いということです。代わりに、通常のアプリケーションロジックを制御する最初のクラスの方法として、従来の条件付きコンストラクトの代わりに、throw / catchコンストラクトを使用します。

多くの場合、通常のアプリケーションロジックでは、同じアクションが数千回/数百万回繰り返されるループを実行します。この場合、いくつかの非常に単純なプロファイリング(Stopwatchクラスを参照)を使用すると、単純なifステートメントが実質的に遅くなることが判明する代わりに、例外をスローすることがわかります。

実際、私はかつて、Microsoftの.NETチームが多くの基本FCLタイプに.NET 2.0のTryXXXXXメソッドを導入したことをかつて読んだことがありました。

多くの場合、これは顧客がループで値の型変換を試みていて、それぞれの試みが失敗したことが原因でした。変換例外がスローされ、例外ハンドラーによってキャッチされ、例外が飲み込まれてループが続行されました。

マイクロソフトでは、このようなパフォーマンスの問題を回避するために、この状況では特にTryXXXメソッドを使用することを推奨しています。

私は間違っているかもしれませんが、あなたが読んだ「ベンチマーク」の真実性についてあなたは確信がないようです。簡単な解決策:自分で試してみてください。


これらの「try」関数は例外的にも例外を使用すると思いましたか?
集約

1
これらの「試行」関数は、入力値の解析に失敗した場合に内部で例外をスローしません。ただし、ArgumentExceptionなどの他のエラー状況では例外をスローします。
Ash

この答えは、他のどの問題よりも問題の核心に近づくと思います。「妥当な状況でのみ例外を使用する」と言っても、実際には質問に答えることはできません。本当の洞察は、制御フローにC#例外を使用すると、通常の条件構造よりもはるかに遅いということです。そうでなければ考えることを許されるかもしれません。OCamlでは、例外は多かれ少なかれGOTOであり、命令型機能を使用するときにブレークを実装するために受け入れられている方法です。私の特定のケースでは、int.Parse()に加えてtry / catchint.TryParse()をタイトループで置き換えると、パフォーマンスが大幅に向上しました。
ヒューW

4

私がXMPPサーバーを大幅に高速化しました(申し訳ありませんが、実際の数値ではなく、純粋に観測値)。これを防ぐために(ソケットが接続されているかどうかを確認してからデータを読み取ろうとするなど)、それを回避する方法を自分自身で試しました。 (前述のTryXメソッド)。これには、アクティブな(チャット)仮想ユーザーが約50人しかいませんでした。


3
数字残念ながら、有用であろう:(デバッグないときのソケット操作のようなものが非常に確かに、例外コストを上回る必要がありますあなたはベンチマーク完全に、私は本当に結果を確認するために興味があると思います。。
ジョンスキート

3

このディスカッションに私自身の最近の経験を追加するだけです。上記の記述のほとんどと同様に、デバッガーを実行していなくても、繰り返し実行すると例外のスローが非常に遅くなることがわかりました。約5行のコードを変更することで、作成している大規模プログラムのパフォーマンスを60%向上させました。例外をスローする代わりに、リターンコードモデルに切り替えました。確かに、問題のコードは何千回も実行されており、変更する前に何千もの例外をスローする可能性がありました。したがって、私は上記のステートメントに同意します。「予期される」状況でアプリケーションフローを制御する方法としてではなく、重要なものが実際に失敗したときに例外をスローします。


2

それらをリターンコードと比較すると、非常に遅いです。ただし、以前の投稿者が述べたように、通常のプログラム操作を投入したくないので、問題が発生した場合にのみパフォーマンスが低下し、ほとんどの場合、パフォーマンスは問題になりません(例外はとにかくロードブロッキングを意味するため)。

それらは間違いなくエラーコードよりも使用する価値があり、利点は広大なIMOです。


2

例外的なパフォーマンスの問題は一度もありません。私は例外をよく使用します-可能な場合は、リターンコードを使用しません。それらは悪い習慣であり、私の意見では、スパゲッティコードのようなにおいがします。

それはすべて、例外の使用方法に要約されます:リターンコードのようにそれらを使用する場合(スタックの各メソッド呼び出しがキャッチして再スローする)、そうです、そうです、各キャッチ/スローのオーバーヘッドがあるため、それらは遅くなります。

ただし、スタックの一番下でスローして一番上でキャッチする場合(リターンコードのチェーン全体を1つのスロー/キャッチで置き換える)、すべてのコストのかかる操作が一度実行されます。

結局のところ、これらは有効な言語機能です。

私のポイントを証明するためだけに

このリンクでコードを実行してください(回答するには大きすぎます)。

私のコンピューター上の結果:

marco@sklivvz:~/develop/test$ mono Exceptions.exe | grep PM
10/2/2008 2:53:32 PM
10/2/2008 2:53:42 PM
10/2/2008 2:53:52 PM

タイムスタンプは、戻りコードと例外の間の最初、最後に出力されます。どちらの場合も同じ時間がかかります。最適化を使用してコンパイルする必要があることに注意してください。


2

ただし、モノは.netスタンドアロンモードよりも10倍速く例外をスローし、.netスタンドアロンモードは.netデバッガモードよりも60倍速く例外をスローします。(テストマシンは同じCPUモデルを持っています)

int c = 1000000;
int s = Environment.TickCount;
for (int i = 0; i < c; i++)
{
    try { throw new Exception(); }
    catch { }
}
int d = Environment.TickCount - s;

Console.WriteLine(d + "ms / " + c + " exceptions");

1

リリースモードでは、オーバーヘッドは最小限です。

フロー制御の例外(非ローカル出口など)を再帰的に使用するのでない限り、その違いに気付くことはないでしょう。


1

Windows CLRでは、深さ8の呼び出しチェーンの場合、例外のスローは、戻り値の確認と伝播よりも750倍遅くなります。(ベンチマークについては以下を参照)

この例外の高コストは、Windows CLRがWindows構造化例外処理と呼ばれるものと統合されるためです。これにより、さまざまなランタイムや言語間で例外を適切にキャッチしてスローすることができます。しかし、それは非常に遅いです。

(すべてのプラットフォームの)Monoランタイムの例外は、SEHと統合されていないため、はるかに高速です。ただし、SEHなどを使用しないため、複数のランタイムに例外を渡すと機能が失われます。

以下は、Windows CLRの例外と戻り値の私のベンチマークからの省略された結果です。

baseline: recurse_depth 8, error_freqeuncy 0 (0), time elapsed 13.0007 ms
baseline: recurse_depth 8, error_freqeuncy 0.25 (0), time elapsed 13.0007 ms
baseline: recurse_depth 8, error_freqeuncy 0.5 (0), time elapsed 13.0008 ms
baseline: recurse_depth 8, error_freqeuncy 0.75 (0), time elapsed 13.0008 ms
baseline: recurse_depth 8, error_freqeuncy 1 (0), time elapsed 14.0008 ms
retval_error: recurse_depth 5, error_freqeuncy 0 (0), time elapsed 13.0008 ms
retval_error: recurse_depth 5, error_freqeuncy 0.25 (249999), time elapsed 14.0008 ms
retval_error: recurse_depth 5, error_freqeuncy 0.5 (499999), time elapsed 16.0009 ms
retval_error: recurse_depth 5, error_freqeuncy 0.75 (999999), time elapsed 16.001 ms
retval_error: recurse_depth 5, error_freqeuncy 1 (999999), time elapsed 16.0009 ms
retval_error: recurse_depth 8, error_freqeuncy 0 (0), time elapsed 20.0011 ms
retval_error: recurse_depth 8, error_freqeuncy 0.25 (249999), time elapsed 21.0012 ms
retval_error: recurse_depth 8, error_freqeuncy 0.5 (499999), time elapsed 24.0014 ms
retval_error: recurse_depth 8, error_freqeuncy 0.75 (999999), time elapsed 24.0014 ms
retval_error: recurse_depth 8, error_freqeuncy 1 (999999), time elapsed 24.0013 ms
exception_error: recurse_depth 8, error_freqeuncy 0 (0), time elapsed 31.0017 ms
exception_error: recurse_depth 8, error_freqeuncy 0.25 (249999), time elapsed 5607.3208     ms
exception_error: recurse_depth 8, error_freqeuncy 0.5 (499999), time elapsed 11172.639  ms
exception_error: recurse_depth 8, error_freqeuncy 0.75 (999999), time elapsed 22297.2753 ms
exception_error: recurse_depth 8, error_freqeuncy 1 (999999), time elapsed 22102.2641 ms

そして、これがコードです。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace ConsoleApplication1 {

public class TestIt {
    int value;

    public class TestException : Exception { } 

    public int getValue() {
        return value;
    }

    public void reset() {
        value = 0;
    }

    public bool baseline_null(bool shouldfail, int recurse_depth) {
        if (recurse_depth <= 0) {
            return shouldfail;
        } else {
            return baseline_null(shouldfail,recurse_depth-1);
        }
    }

    public bool retval_error(bool shouldfail, int recurse_depth) {
        if (recurse_depth <= 0) {
            if (shouldfail) {
                return false;
            } else {
                return true;
            }
        } else {
            bool nested_error = retval_error(shouldfail,recurse_depth-1);
            if (nested_error) {
                return true;
            } else {
                return false;
            }
        }
    }

    public void exception_error(bool shouldfail, int recurse_depth) {
        if (recurse_depth <= 0) {
            if (shouldfail) {
                throw new TestException();
            }
        } else {
            exception_error(shouldfail,recurse_depth-1);
        }

    }

    public static void Main(String[] args) {
        int i;
        long l;
        TestIt t = new TestIt();
        int failures;

        int ITERATION_COUNT = 1000000;


        // (0) baseline null workload
        for (int recurse_depth = 2; recurse_depth <= 10; recurse_depth+=3) {
            for (float exception_freq = 0.0f; exception_freq <= 1.0f; exception_freq += 0.25f) {            
                int EXCEPTION_MOD = (exception_freq == 0.0f) ? ITERATION_COUNT+1 : (int)(1.0f / exception_freq);            

                failures = 0;
                DateTime start_time = DateTime.Now;
                t.reset();              
                for (i = 1; i < ITERATION_COUNT; i++) {
                    bool shoulderror = (i % EXCEPTION_MOD) == 0;
                    t.baseline_null(shoulderror,recurse_depth);
                }
                double elapsed_time = (DateTime.Now - start_time).TotalMilliseconds;
                Console.WriteLine(
                    String.Format(
                      "baseline: recurse_depth {0}, error_freqeuncy {1} ({2}), time elapsed {3} ms",
                        recurse_depth, exception_freq, failures,elapsed_time));
            }
        }


        // (1) retval_error
        for (int recurse_depth = 2; recurse_depth <= 10; recurse_depth+=3) {
            for (float exception_freq = 0.0f; exception_freq <= 1.0f; exception_freq += 0.25f) {            
                int EXCEPTION_MOD = (exception_freq == 0.0f) ? ITERATION_COUNT+1 : (int)(1.0f / exception_freq);            

                failures = 0;
                DateTime start_time = DateTime.Now;
                t.reset();              
                for (i = 1; i < ITERATION_COUNT; i++) {
                    bool shoulderror = (i % EXCEPTION_MOD) == 0;
                    if (!t.retval_error(shoulderror,recurse_depth)) {
                        failures++;
                    }
                }
                double elapsed_time = (DateTime.Now - start_time).TotalMilliseconds;
                Console.WriteLine(
                    String.Format(
                      "retval_error: recurse_depth {0}, error_freqeuncy {1} ({2}), time elapsed {3} ms",
                        recurse_depth, exception_freq, failures,elapsed_time));
            }
        }

        // (2) exception_error
        for (int recurse_depth = 2; recurse_depth <= 10; recurse_depth+=3) {
            for (float exception_freq = 0.0f; exception_freq <= 1.0f; exception_freq += 0.25f) {            
                int EXCEPTION_MOD = (exception_freq == 0.0f) ? ITERATION_COUNT+1 : (int)(1.0f / exception_freq);            

                failures = 0;
                DateTime start_time = DateTime.Now;
                t.reset();              
                for (i = 1; i < ITERATION_COUNT; i++) {
                    bool shoulderror = (i % EXCEPTION_MOD) == 0;
                    try {
                        t.exception_error(shoulderror,recurse_depth);
                    } catch (TestException e) {
                        failures++;
                    }
                }
                double elapsed_time = (DateTime.Now - start_time).TotalMilliseconds;
                Console.WriteLine(
                    String.Format(
                      "exception_error: recurse_depth {0}, error_freqeuncy {1} ({2}), time elapsed {3} ms",
                        recurse_depth, exception_freq, failures,elapsed_time));         }
        }
    }
}


}

5
質問のポイントを逃すことは別として、ベンチマークにはDateTime.Nowを使用しないでください。経過時間を測定するために設計されたストップウォッチを使用してください。かなり長い時間を測定しているので、ここでは問題になりませんが、習慣に入る価値はあります。
Jon Skeet、2012年

それどころか、問題は「例外は遅い」、期間です。特に、例外をスローするタイミングのトピックは事実を覆い隠すため、このトピックを回避するように求めました。例外のパフォーマンスは何ですか?
David Jeske 16

0

ここで、例外のキャッチに関連するパフォーマンスについて簡単に説明します。

実行パスが「try」ブロックに入ると、魔法のようなことは起こりません。「try」命令はなく、tryブロックの開始または終了に関連するコストもありません。tryブロックに関する情報はメソッドのメタデータに格納され、このメタデータは例外が発生するたびに実行時に使用されます。実行エンジンはスタックをたどり、tryブロックに含まれていた最初の呼び出しを探します。例外処理に関連するオーバーヘッドは、例外がスローされたときにのみ発生します。


1
ただし、例外の存在は最適化に影響を与える可能性があります。明示的な例外ハンドラーを持つメソッドはインライン化することが難しく、命令の順序変更はそれらによって制限されます。
Eamon Nerbonne、2010

-1

他の人が使用するためのクラス/関数を書くとき、例外が適切であるときを言うのは難しいようです。エラーを返す代わりに例外をスローするため、BCLの便利な部分がいくつかあります。場合によってはそれを回避できますが、System.Managementやパフォーマンスカウンターなどの場合は、BCLによって例外が頻繁にスローされるループを実行する必要がある使用法があります。

ライブラリを作成していて、関数がループで使用される可能性があり、大量の反復が発生する可能性がある場合は、Try ..パターンまたはその他の方法を使用して、例外以外のエラーを公開します。それでも、共有環境の多くのプロセスで使用されている場合、関数がどれだけ呼び出されるかを判断するのは困難です。

私のコードでは、例外は非常に例外的な場合にのみスローされ、スタックトレースを調べて問題の原因を確認し、修正する必要があります。そのため、例外の代わりにTry ..パターンに基づくエラー処理を使用するようにBCLの一部を書き直しました。


2
これは、ポスターの「例外をスローするタイミングとスローしないことについての議論はしたくない」というステートメントには適合しないようです。
hrbrmstr 2014年
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.