例外をスローするタイミングとスローしないタイミングについての議論は望まない。簡単な問題を解決したいです。例外をスローしないという議論は、99%が例外をスローすることを中心に行われますが、反対側は(ベンチマークテストで)速度は問題ではないと主張します。どちらか一方に関するブログ、記事、投稿を多数読んでいます。それでどちらですか?
私は「遅くない」側にいる-より正確には「通常の使用でそれらを避ける価値があるほど遅くない」。これについて2つの shortarticles を書いた。ベンチマークの側面に対する批判がありますが、これは主に「実際にはもっと多くのスタックを通過させる必要があるため、キャッシュを吹き飛ばす」などです。 alsoキャッシュを爆破するので、それは特に良い議論だとは思わない。
明確にするために、例外が論理的でない場合は例外の使用をサポートしていません。例えば、 int.TryParse
は、ユーザーからのデータの変換に完全に適しています。失敗は「ファイルが本来の形式になっていないため、他に何が間違っているのかわからないので、これを処理したくありません。 」
「合理的な状況のみ」で例外を使用する場合、例外によってパフォーマンスが著しく低下したアプリケーションを見たことはありません。基本的に、重大な正確性の問題がない限り、例外は頻繁に発生することはありません。重大な正確性の問題がある場合、パフォーマンスは直面する最大の問題ではありません。
これに対する決定的な答えは、それらを実装した人、クリス・ブラムからです。彼はこのテーマについて 優秀なブログ記事 を書いた(警告-非常に長い)(警告2-とてもよく書かれている、技術者なら最後まで読んでから作らなければならない仕事の後のあなたの時間まで:))
エグゼクティブサマリー:遅いです。それらはWin32 SEH例外として実装されているため、一部はリング0のCPU境界を通過することさえあります!明らかに現実の世界では、他の多くの作業を行うので、奇妙な例外はまったく気付かれませんが、それらをアプリのハンマー以外のプログラムフローに使用すると、例外が発生します。これは、MSマーケティングマシンが私たちに損害を与えているもう1つの例です。私は、Microsoftがオーバーヘッドをまったくゼロにした方法を教えてくれたことを思い出します。
クリスは適切な引用を与えます:
実際、CLRはエンジンの管理されていない部分でも例外を内部的に使用します。ただし、例外を伴う深刻な長期的なパフォーマンスの問題があるため、これを考慮に入れる必要があります。
スローされた場合にのみ遅いと言うとき、人々が何について話しているのか分かりません。
編集:例外がスローされない場合、それは新しいException()またはそのようなことをしていることを意味します。そうでない場合、例外によりスレッドが中断され、スタックがウォークされます。 be Ok小規模な状況ではありますが、トラフィックの多いWebサイトでは、ワークフローまたは実行パスメカニズムとして例外に依存すると、パフォーマンスの問題が発生します。例外自体は悪くありません。例外的な条件を表現するのに便利です
.NETアプリの例外ワークフローは、ファーストチャンスとセカンドチャンスの例外を使用します。すべての例外について、それらをキャッチして処理している場合でも、例外オブジェクトは作成されたままであり、フレームワークはハンドラーを探すためにスタックを歩く必要があります。もちろん、キャッチして再スローすると時間がかかります-最初のチャンスの例外を取得し、それをキャッチして再スローすると、別の最初のチャンスの例外が発生し、ハンドラが見つかりません。セカンドチャンスの例外。
例外もヒープ上のオブジェクトです。したがって、大量の例外をスローしている場合、パフォーマンスとメモリの両方の問題が発生しています。
さらに、ACEチームが作成した「Microsoft .NET Webアプリケーションのパフォーマンステスト」の私のコピーによると、
「例外処理はコストがかかります。関連するスレッドの実行は、CLRが適切な例外ハンドラーを探して呼び出しスタックを再帰処理している間中断されます。見つかった場合、例外ハンドラーといくつかのfinallyブロックはすべて実行する機会が必要通常の処理を実行する前に。」
この分野での私自身の経験から、例外を減らすことでパフォーマンスが大幅に向上することがわかりました。もちろん、パフォーマンステストの際に考慮する他の事項があります。たとえば、ディスクI/Oが撮影された場合、またはクエリが数秒以内にある場合、それが焦点になります。ただし、例外を見つけて削除することは、その戦略の重要な部分です。
私が理解している議論は、例外をスローすること自体が遅いということではありません。代わりに、従来の条件付きコンストラクトではなく、通常のアプリケーションロジックを制御するファーストクラスの方法としてthrow/catchコンストラクトを使用することです。
多くの場合、通常のアプリケーションロジックでは、同じアクションが何千/何百万回繰り返されるループを実行します。この場合、非常に単純なプロファイリング(Stopwatchクラスを参照)を使用すると、単純なifステートメントの代わりに例外をスローすると、かなり遅くなることがわかります。
実際、Microsoftの.NETチームが.NET 2.0のTryXXXXXメソッドを多くの基本FCLタイプに導入したことを一度読んだことがあります。これは、顧客がアプリケーションのパフォーマンスが非常に遅いと訴えているためです。
多くの場合、これは顧客がループ内で値の型変換を試行し、各試行が失敗したことが原因でした。変換例外がスローされ、例外ハンドラーによってキャッチされた後、例外を飲み込んでループを継続しました。
マイクロソフトでは、このようなパフォーマンスの問題を回避するために、特にこの状況でTryXXXメソッドを使用することを推奨しています。
私は間違っているかもしれませんが、あなたが読んだ「ベンチマーク」の真実性についてあなたは確信していないようです。簡単な解決策:自分で試してみてください。
XMPPサーバーは、データの読み取りを試みる前にソケットが接続されているかどうかを確認するなど)一貫して発生を防ぎ、それらを回避する方法を与えた後、XMPPサーバーが大幅な速度向上(申し訳ありませんが、実際の数値なし、純粋に観測的)を獲得しました(前述のTryXメソッド)。これは、アクティブ(チャット)の仮想ユーザーが約50人だけでした。
この議論に自分自身の最近の経験を追加するだけです。上記の内容のほとんどに沿って、デバッガーを実行しなくても、繰り返し実行すると例外のスローが非常に遅いことがわかりました。約5行のコードを変更することで、書いている大規模なプログラムのパフォーマンスを60%向上させました。例外をスローする代わりにリターンコードモデルに切り替えます。確かに、問題のコードは何千回も実行されており、変更する前に何千もの例外をスローする可能性がありました。したがって、私は上記の文に同意します。「予想される」状況でアプリケーションフローを制御する方法としてではなく、重要な何かが実際にうまくいかないときに例外をスローします。
ただし、monoは.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回行われます。
結局のところ、これらは有効な言語機能です。
私のポイントを証明するだけです
このリンクのコード (回答するには大きすぎます)を実行してください。
私のコンピューターでの結果:
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
タイムスタンプは、最初に、戻りコードと例外の間で、最後に出力されます。どちらの場合も同じ時間がかかります。最適化してコンパイルする必要があることに注意してください。
それらを戻りコードと比較すると、それらは地獄のように遅いです。ただし、以前のポスターで述べたように、通常のプログラム操作では投げたくないので、問題が発生した場合にのみパフォーマンスが低下し、ほとんどの場合、パフォーマンスは問題になりません(例外はとにかくロードブロッキングを意味するため)。
これらは間違いなくエラーコードよりも使用する価値があり、その利点はIMOの広大さです。
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ブロックに含まれていた最初の呼び出しを探してスタックをたどります。例外処理に関連するオーバーヘッドは、例外がスローされたときにのみ発生します。