例外をスローするタイミングとスローしないタイミングについての議論はしたくありません。簡単な問題を解決したいと思います。例外をスローしないという主張は99%の確率でスローされますが、反対側は(ベンチマークテストを使用して)速度は問題ではないと主張します。私は、どちらか一方に関係する多数のブログ、記事、および投稿を読みました。どっち?
例外をスローするタイミングとスローしないタイミングについての議論はしたくありません。簡単な問題を解決したいと思います。例外をスローしないという主張は99%の確率でスローされますが、反対側は(ベンチマークテストを使用して)速度は問題ではないと主張します。私は、どちらか一方に関係する多数のブログ、記事、および投稿を読みました。どっち?
回答:
私は「遅くない」側にいます-より正確には「通常の使用でそれらを避ける価値があるほど遅くない」のです。これについて2つの短い 記事を書きました。ベンチマークの側面への批判がありますが、ほとんどは「実際にはもっと多くのスタックを通過する必要があるため、キャッシュをブローするなど」ですが、エラーコードを使用してスタックを上に移動することもできます。キャッシュをブローするので、これは特に良い議論だとは思いません。
明確にするために-論理的でない例外の使用はサポートしていません。たとえばint.TryParse
、ユーザーからのデータの変換には完全に適しています。機械で生成されたファイルを読み取る場合に適しています。失敗とは、「ファイルが本来の形式ではないため、他に何が問題なのかわからないので、これを処理したくない場合です。 」
「合理的な状況のみ」で例外を使用する場合、例外によってパフォーマンスが大幅に低下するアプリケーションを見たことがありません。基本的に、重大な正確性の問題がない限り、例外は頻繁に発生しないはずです。重大な正確性の問題がある場合、パフォーマンスは直面する最大の問題ではありません。
それらを実装した人、Chris Brummeからの決定的な答えがあります。彼は件名について優れたブログ記事(警告-その非常に長い)(警告2-非常によく書かれている、あなたが技術者なら最後まで読んでから仕事の後の時間を補わなければならない:)を書いた。 )
要旨:彼らは遅い。それらはWin32 SEH例外として実装されているため、一部はリング0のCPU境界を通過します。明らかに現実の世界では、他の多くの作業を行うことになるので、奇妙な例外はまったく気付かれませんが、それらをプログラムフローに使用する場合は、アプリがハンマーで打たれることを期待してください。これは、MSマーケティングマシンが私たちに害を及ぼすもう1つの例です。オーバーヘッドがまったくゼロになり、完全に無事だった方法を教えてくれたマイクロソフト社を思い出します。
クリスは適切な引用をします:
実際、CLRは内部的に、エンジンの管理されていない部分でも例外を使用します。ただし、例外を伴う深刻な長期的なパフォーマンスの問題があり、これを決定に含める必要があります。
スローされたときだけ遅いと言っているとき、私は人々が何について話しているのかわかりません。
編集:例外がスローされない場合は、新しいException()などを実行していることを意味します。それ以外の場合は、例外によってスレッドが中断され、スタックがウォークされます。これは小さな状況では問題ないかもしれませんが、トラフィックの多いWebサイトでは、ワークフローまたは実行パスのメカニズムが例外であるため、パフォーマンスの問題が確実に発生します。例外自体は悪くないので、例外的な条件を表現するのに役立ちます
.NETアプリの例外ワークフローは、ファーストチャンスとセカンドチャンスの例外を使用します。すべての例外について、それらをキャッチして処理している場合でも、例外オブジェクトは引き続き作成され、フレームワークはスタックを走査してハンドラーを探す必要があります。もちろん、キャッチして再スローするのに時間がかかります-初回例外が発生し、それをキャッチして再スローすると、別の初回例外が発生し、ハンドラが見つかりません。セカンドチャンス例外。
例外はヒープ上のオブジェクトでもあるため、大量の例外をスローすると、パフォーマンスとメモリの両方の問題が発生します。
さらに、ACEチームが執筆した「Microsoft .NET Webアプリケーションのパフォーマンステスト」によると、
「例外処理にはコストがかかります。CLRが正しい例外ハンドラーを探してコールスタックを再帰している間、関連するスレッドの実行が中断されます。例外が見つかると、例外ハンドラーといくつかのfinallyブロックがすべて実行されるチャンスがなければなりません。通常の処理を行う前に」
この分野での私自身の経験から、例外を減らすことでパフォーマンスが大幅に向上することがわかりました。もちろん、パフォーマンステストの際に考慮すべき他の事項があります。たとえば、ディスクI / Oが実行された場合、またはクエリが数秒で実行された場合、それが焦点になります。ただし、例外を見つけて削除することは、その戦略の重要な部分です。
私が理解している議論は、例外のスローが悪いということではなく、それ自体が遅いということです。代わりに、通常のアプリケーションロジックを制御する最初のクラスの方法として、従来の条件付きコンストラクトの代わりに、throw / catchコンストラクトを使用します。
多くの場合、通常のアプリケーションロジックでは、同じアクションが数千回/数百万回繰り返されるループを実行します。この場合、いくつかの非常に単純なプロファイリング(Stopwatchクラスを参照)を使用すると、単純なifステートメントが実質的に遅くなることが判明する代わりに、例外をスローすることがわかります。
実際、私はかつて、Microsoftの.NETチームが多くの基本FCLタイプに.NET 2.0のTryXXXXXメソッドを導入したことをかつて読んだことがありました。
多くの場合、これは顧客がループで値の型変換を試みていて、それぞれの試みが失敗したことが原因でした。変換例外がスローされ、例外ハンドラーによってキャッチされ、例外が飲み込まれてループが続行されました。
マイクロソフトでは、このようなパフォーマンスの問題を回避するために、この状況では特にTryXXXメソッドを使用することを推奨しています。
私は間違っているかもしれませんが、あなたが読んだ「ベンチマーク」の真実性についてあなたは確信がないようです。簡単な解決策:自分で試してみてください。
私がXMPPサーバーを大幅に高速化しました(申し訳ありませんが、実際の数値ではなく、純粋に観測値)。これを防ぐために(ソケットが接続されているかどうかを確認してからデータを読み取ろうとするなど)、それを回避する方法を自分自身で試しました。 (前述のTryXメソッド)。これには、アクティブな(チャット)仮想ユーザーが約50人しかいませんでした。
このディスカッションに私自身の最近の経験を追加するだけです。上記の記述のほとんどと同様に、デバッガーを実行していなくても、繰り返し実行すると例外のスローが非常に遅くなることがわかりました。約5行のコードを変更することで、作成している大規模プログラムのパフォーマンスを60%向上させました。例外をスローする代わりに、リターンコードモデルに切り替えました。確かに、問題のコードは何千回も実行されており、変更する前に何千もの例外をスローする可能性がありました。したがって、私は上記のステートメントに同意します。「予期される」状況でアプリケーションフローを制御する方法としてではなく、重要なものが実際に失敗したときに例外をスローします。
例外的なパフォーマンスの問題は一度もありません。私は例外をよく使用します-可能な場合は、リターンコードを使用しません。それらは悪い習慣であり、私の意見では、スパゲッティコードのようなにおいがします。
それはすべて、例外の使用方法に要約されます:リターンコードのようにそれらを使用する場合(スタックの各メソッド呼び出しがキャッチして再スローする)、そうです、そうです、各キャッチ/スローのオーバーヘッドがあるため、それらは遅くなります。
ただし、スタックの一番下でスローして一番上でキャッチする場合(リターンコードのチェーン全体を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
タイムスタンプは、戻りコードと例外の間の最初、最後に出力されます。どちらの場合も同じ時間がかかります。最適化を使用してコンパイルする必要があることに注意してください。
ただし、モノは.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");
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)); }
}
}
}
}
ここで、例外のキャッチに関連するパフォーマンスについて簡単に説明します。
実行パスが「try」ブロックに入ると、魔法のようなことは起こりません。「try」命令はなく、tryブロックの開始または終了に関連するコストもありません。tryブロックに関する情報はメソッドのメタデータに格納され、このメタデータは例外が発生するたびに実行時に使用されます。実行エンジンはスタックをたどり、tryブロックに含まれていた最初の呼び出しを探します。例外処理に関連するオーバーヘッドは、例外がスローされたときにのみ発生します。
他の人が使用するためのクラス/関数を書くとき、例外が適切であるときを言うのは難しいようです。エラーを返す代わりに例外をスローするため、BCLの便利な部分がいくつかあります。場合によってはそれを回避できますが、System.Managementやパフォーマンスカウンターなどの場合は、BCLによって例外が頻繁にスローされるループを実行する必要がある使用法があります。
ライブラリを作成していて、関数がループで使用される可能性があり、大量の反復が発生する可能性がある場合は、Try ..パターンまたはその他の方法を使用して、例外以外のエラーを公開します。それでも、共有環境の多くのプロセスで使用されている場合、関数がどれだけ呼び出されるかを判断するのは困難です。
私のコードでは、例外は非常に例外的な場合にのみスローされ、スタックトレースを調べて問題の原因を確認し、修正する必要があります。そのため、例外の代わりにTry ..パターンに基づくエラー処理を使用するようにBCLの一部を書き直しました。