C# の try/catch の実際のオーバーヘッドはどれくらいですか?
-
09-06-2019 - |
質問
Try/Catch はオーバーヘッドを追加するため、プロセス フローを制御する良い方法ではないことはわかっていますが、このオーバーヘッドはどこから来て、実際の影響は何でしょうか?
解決
私は言語実装の専門家ではありません (したがって、これについては割り引いて聞いてください) が、最大のコストの 1 つは、スタックを巻き戻してスタック トレース用に保存することだと思います。これは例外がスローされた場合にのみ発生するのではないかと思います (ただし、わかりません)。もしそうであれば、これは例外がスローされるたびに適切なサイズの隠れたコストになるでしょう...したがって、コード内のある場所から別の場所にジャンプするだけではなく、多くの処理が行われます。
EXCEPTIONAL の動作に対して例外を使用している限り、これは問題ないと思います (つまり、プログラムの通常の予想されるパスではありません)。
他のヒント
ここで注意すべき点は 3 つあります。
まず、実際にコード内に try-catch ブロックを含めても、パフォーマンスに悪影響はほとんどありません。アプリケーションにそれらを含めないようにする場合は、これを考慮すべきではありません。パフォーマンスへの影響は、例外がスローされた場合にのみ発生します。
他の人が言及したスタックの巻き戻し操作などに加えて例外がスローされた場合、スタックトレースなどの例外クラスのメンバーを設定するためにランタイム/リフレクション関連の処理が大量に発生することに注意する必要があります。オブジェクトやさまざまな型メンバーなど。
これが、例外を再スローする場合の一般的なアドバイスが次のとおりである理由の 1 つであると思います。
throw;
例外を再度スローしたり、新しい例外を構築したりするのではなく、そのような場合にはすべてのスタック情報が再収集されますが、単純なスローではすべてが保持されます。
例外がスローされない場合に try/catch/finally を使用する場合のオーバーヘッドについて質問しているのですか、それともプロセス フローを制御するために例外を使用する場合のオーバーヘッドについて質問していますか?後者は、ダイナマイトの棒を使って幼児の誕生日のろうそくに火をつけることに似ており、関連するオーバーヘッドは次の領域に分類されます。
- 通常はキャッシュ内にない常駐データにアクセスするスローされた例外により、さらにキャッシュ ミスが発生することが予想されます。
通常はアプリケーションのワーキング セットにない非常駐コードやデータにアクセスする例外がスローされるため、追加のページ フォールトが発生することが予想されます。
- たとえば、例外をスローするには、CLR が、例外が処理されるまでの各フレームの現在の IP と戻り IP に基づいて、finally ブロックと catch ブロックの場所とフィルター ブロックを見つける必要があります。
- メタデータの読み取りなどの診断目的でフレームを作成するには、追加の構築コストと名前解決が必要です。
上記の項目はどちらも通常、「コールド」コードとデータにアクセスするため、メモリ不足がある場合はハード ページ フォールトが発生する可能性があります。
- CLR は局所性を高めるために、使用頻度が低いコードとデータを頻繁に使用されるデータから遠くに配置しようとします。そのため、冷たいものを熱いものに強制することになり、これは不利になります。
- ハード ページ フォールトがあったとしても、そのコストは他のすべてのコストに比べて小さく見えます。
- 典型的なキャッチ状況は深いことが多いため、上記の影響が拡大する傾向があります (ページフォールトの可能性が増加します)。
コストの実際の影響については、その時点でコード内で他に何が起こっているかによって大きく異なります。ジョン・スキートは ここに良い要約があります, 、役立つリンクがいくつかあります。例外がパフォーマンスに重大な悪影響を与えるという点に達すると、パフォーマンスだけでなく例外の使用に関して問題が発生するという彼の意見に、私は同意する傾向があります。
私の経験では、最大のオーバーヘッドは、実際に例外をスローしてそれを処理する際に発生します。私はかつて、誰かがオブジェクトを編集する権利を持っているかどうかを確認するために、次のようなコードが使用されていたプロジェクトに取り組んでいました。この HasRight() メソッドはプレゼンテーション層のあらゆる場所で使用され、数百のオブジェクトに対して呼び出されることがよくありました。
bool HasRight(string rightName, DomainObject obj) {
try {
CheckRight(rightName, obj);
return true;
}
catch (Exception ex) {
return false;
}
}
void CheckRight(string rightName, DomainObject obj) {
if (!_user.Rights.Contains(rightName))
throw new Exception();
}
テスト データベースがテスト データでいっぱいになると、新しいフォームを開くときなどに目に見える速度の低下が発生します。
そこで、これを次のようにリファクタリングしました。後のクイックアンドダーティ測定によると、約 2 桁高速になりました。
bool HasRight(string rightName, DomainObject obj) {
return _user.Rights.Contains(rightName);
}
void CheckRight(string rightName, DomainObject obj) {
if (!HasRight(rightName, obj))
throw new Exception();
}
つまり、通常のプロセス フローで例外を使用すると、例外を使用せずに同様のプロセス フローを使用した場合よりも約 2 桁遅くなります。
言うまでもなく、頻繁に呼び出されるメソッド内にある場合は、アプリケーション全体の動作に影響を与える可能性があります。
たとえば、Int32.Parse の使用は、他の方法では簡単にキャッチできる例外をスローするため、ほとんどの場合悪い習慣であると考えています。
ここに書かれたことをすべて結論付けると、次のようになります。
1) try..catch ブロックを使用して予期しないエラーを検出します。パフォーマンスはほとんど低下しません。
2) 例外を回避できる場合は、例外エラーには例外を使用しないでください。
当時これについて質問する人が多かったので、少し前にこれについての記事を書きました。これとテストコードは次の場所にあります。 http://www.blackwasp.co.uk/SpeedTestTryCatch.aspx.
結論として、try/catch ブロックには少量のオーバーヘッドがありますが、非常に小さいので無視する必要があります。ただし、何百万回も実行されるループ内で try/catch ブロックを実行している場合は、可能であればブロックをループの外に移動することを検討することをお勧めします。
try/catch ブロックのパフォーマンスに関する重要な問題は、実際に例外をキャッチするときです。これにより、アプリケーションに顕著な遅延が生じる可能性があります。もちろん、物事がうまくいかない場合、ほとんどの開発者 (そして多くのユーザー) は、一時停止がこれから起こる例外であると認識します。ここで重要なのは、通常の操作に例外処理を使用しないことです。名前が示すように、それらは例外的なものであるため、投げられないようにできる限りのことを行う必要があります。正しく機能するプログラムの予期されるフローの一部としてこれらを使用しないでください。
私が作った ブログの記事 去年のこのテーマについて。それをチェックしてください。結論としては、例外が発生しなければ、try ブロックのコストはほとんどかからないということです。私のラップトップでは、例外は約 36μs でした。これは予想よりも少ないかもしれませんが、これらの結果は浅いスタックにあることに留意してください。また、最初の例外は非常に遅いです。
コンパイラ エラー メッセージ、コード分析の警告メッセージ、およびルーチンで受け入れられる例外 (特に、ある場所でスローされ、別の場所で受け入れられる例外) が発生しないコードの作成、デバッグ、および保守が非常に簡単になります。簡単なため、コードは平均してより適切に記述され、バグが少なくなります。
私にとって、プログラマーと品質のオーバーヘッドが、プロセス フローに try-catch を使用することに反対する主な議論です。
例外によるコンピューターのオーバーヘッドはそれに比べれば取るに足らないものであり、実際のパフォーマンス要件を満たすアプリケーションの能力という観点から見れば、通常はごくわずかです。
一般に受け入れられている理論に反して、 try
/catch
パフォーマンスに重大な影響を与える可能性があり、例外がスローされるかどうかが影響します。
- 一部の自動最適化が (仕様により) 無効になります。, 、場合によっては注入します デバッグ コードから予想できるように、 デバッグ支援. 。この点に関して私に同意しない人は常にいますが、言語ではそれが必要であり、逆アセンブリでそれが示されるため、それらの人々は辞書の定義に従っています 妄想的な.
- メンテナンス時に悪影響を及ぼす可能性があります。 実際、これがここで最も重要な問題ですが、私の前回の回答 (ほぼ完全にそれに焦点を当てた) が削除されたため、より重要な問題ではなく、それほど重要ではない問題 (マイクロ最適化) に焦点を当ててみます。マクロ最適化)。
前者については、長年にわたって Microsoft MVP によるいくつかのブログ投稿で取り上げられており、簡単に見つけられると思いますが、StackOverflow は気にしています そんなに について コンテンツ したがって、それらのいくつかへのリンクを提供します フィラー 証拠:
- パフォーマンスへの影響
try
/catch
/finally
(そしてパート2)、Peter Ritchie による最適化を検討しています。try
/catch
/finally
無効にします (標準からの引用を使用してさらに詳しく説明します) - パフォーマンスプロファイリング
Parse
対TryParse
対ConvertTo
Ian Huff 氏は「例外処理が非常に遅い」とあからさまに述べており、この点をピットで示しています。Int.Parse
そしてInt.TryParse
互いに対して...そう主張する人へTryParse
用途try
/catch
舞台裏に光が当たるはずです!
それもあります この答え これは、を使用した場合と使用しない場合の逆アセンブルされたコードの違いを示しています。 try
/catch
.
とても明白に思えますが、 は これはコード生成時に明らかに観察できるオーバーヘッドであり、そのオーバーヘッドは Microsoft が評価している人々さえも認めているようです。それでも私は、 インターネットを繰り返す...
はい、1 つの些細なコード行に数十の追加の MSIL 命令があり、無効化された最適化さえカバーしていないため、技術的にはマイクロ最適化です。
私は数年前に回答を投稿しましたが、プログラマーの生産性 (マクロ最適化) に焦点を当てていたため、削除されました。
これは残念なことです。CPU 時間を数ナノ秒節約しただけでは、人間による手動最適化の累積時間を補うことはできないでしょう。あなたの上司はどちらに多くお金を払っていますか?自分の時間は 1 時間ですか、それともコンピューターを実行している時間は 1 時間ですか?どの時点で私たちはプラグを抜き、ただする時が来たと認めるでしょうか? より高速なコンピュータを購入する?
明らかに、私たちはそうあるべきです 優先順位を最適化する, 、コードだけではありません。前回の回答では、2 つのコード断片の違いを取り上げました。
使用する try
/catch
:
int x;
try {
x = int.Parse("1234");
}
catch {
return;
}
// some more code here...
使用していない try
/catch
:
int x;
if (int.TryParse("1234", out x) == false) {
return;
}
// some more code here
メンテナンス開発者の観点から考えてみましょう。プロファイリング/最適化 (上記で説明) がなければ時間を無駄にする可能性が高くなります。 try
/catch
問題が発生し、ソースコードをスクロールすると...そのうちの 1 つには、定型文の不要な行が 4 行追加されています。
クラスに導入されるフィールドが増えるにつれて、このボイラープレートのゴミはすべて (ソース コードと逆アセンブル コードの両方で) 妥当なレベルをはるかに超えて蓄積されます。フィールドごとに 4 行の追加行があり、それらは常に同じ行です...私たちは同じことを繰り返さないように教えられなかったのでしょうか?隠蔽できると思います try
/catch
独自の抽象化の背後にありますが...それなら、例外を避けたほうがいいかもしれません(すなわち、使用 Int.TryParse
).
これは複雑な例ですらない。新しいクラスをインスタンス化しようとする試みを見てきました try
/catch
. 。コンストラクター内のすべてのコードが、コンパイラーによって自動的に適用される特定の最適化から除外される可能性があることを考慮してください。という理論を生み出すこれより良い方法はないだろうか コンパイラが遅い, 、 とは対照的に コンパイラは指示されたことを正確に実行します?
コンストラクターによって例外がスローされ、その結果として何らかのバグがトリガーされたと仮定すると、保守が不十分な開発者はそれを追跡する必要があります。それは、スパゲッティ コードとは異なり、それほど簡単な作業ではないかもしれません。 後藤 悪夢、 try
/catch
混乱を引き起こす可能性があります 三次元, スタックを同じメソッドの他の部分だけでなく、他のクラスやメソッドにも移動する可能性があるため、それらはすべてメンテナンス開発者によって監視されます。 困難な道!それなのに「gotoは危ない」と言われているんです、へー!
最後に言及しますが、 try
/catch
その利点は、 最適化を無効にするように設計されています!言ってみれば、それは、 デバッグ支援!それがそのために設計されたものであり、それが次のように使用されるべきものです...
それも良い点だと思います。これを使用すると、マルチスレッド アプリケーションの安全で健全なメッセージ パッシング アルゴリズムを機能不全にする可能性がある最適化を無効にしたり、競合状態の可能性をキャッチしたりすることができます ;) try/catch を使用するシナリオは、これが私が思いつく唯一のシナリオです。それにも代替手段はあります。
最適化が行うこと try
, catch
そして finally
無効にする?
別名
調子はどうですか try
, catch
そして finally
デバッグ補助として役立つでしょうか?
それらは書き込み障壁です。これは標準から来ています:
12.3.3.13 Try-catch ステートメント
声明の場合 stmt 形式:
try try-block catch ( ... ) catch-block-1 ... catch ( ... ) catch-block-n
- 明確な割り当て状態 v の初めに トライブロック の明確な代入状態と同じです v の初めに stmt.
- 明確な割り当て状態 v の初めに キャッチブロック-i (どんな場合でも 私) の明確な代入状態と同じです。 v の初めに stmt.
- 明確な割り当て状態 v の終点で stmt 確実に割り当てられるのは次の場合 (その場合に限り) v のエンドポイントに確実に割り当てられています トライブロック そしてすべての キャッチブロック-i (すべてのための 私 1から n).
つまり、それぞれの始まりに、 try
声明:
- に入る前に表示されているオブジェクトに対して行われたすべての割り当て
try
ステートメントは完了している必要があり、開始するにはスレッド ロックが必要なので、競合状態のデバッグに役立ちます。 - コンパイラでは次のことは許可されていません。
- 前に確実に割り当てられていた未使用の変数割り当てを削除します。
try
声明 - それらのいずれかを再編成または結合する 内部代入 (すなわち、まだ行っていない場合は、最初のリンクを参照してください)。
- このバリアを越えて代入を巻き上げて、(使用されるとしても) 後まで使用されないことがわかっている変数への代入を遅らせたり、他の最適化を可能にするために後の代入を先制的に前方に移動したりできます...
- 前に確実に割り当てられていた未使用の変数割り当てを削除します。
同様の話がそれぞれに当てはまります catch
声明;あなたの中で想像してください try
ステートメント (またはそれが呼び出すコンストラクターや関数など) を、そうでなければ意味のない変数に代入します (たとえば、 garbage=42;
)、プログラムの観察可能な動作にどれほど無関係であっても、コンパイラはそのステートメントを削除することはできません。割り当てには次のものが必要です 完成した の前に catch
ブロックが入ります。
それだけの価値があるのに、 finally
同様に言う 劣化する 話:
12.3.3.14 Try-finally ステートメント
のために 試す 声明 stmt 形式:
try try-block finally finally-block
• の明確な代入状態 v の初めに トライブロック の明確な代入状態と同じです v の初めに stmt.
• の明確な代入状態 v の初めに 最後にブロックする の明確な代入状態と同じです v の初めに stmt.
• の明確な代入状態 v の終点で stmt 次のいずれかの場合 (その場合に限り) は確実に割り当てられます。ああ v のエンドポイントに確実に割り当てられています トライブロックああ v のエンドポイントに確実に割り当てられています 最後にブロックする制御フロー転送 ( 後藤 ステートメント) 内で始まるステートメントが作成されます。 トライブロック, 、の外で終了します。 トライブロック, 、 それから v 次の場合にも、その制御フロー転送に確実に割り当てられているとみなされます。 v のエンドポイントに確実に割り当てられています 最後にブロックする. 。(これは、「もしも」だけの話ではありません。 v この制御フロー転送で別の理由で確実に割り当てられている場合でも、確実に割り当てられているとみなされます。)
12.3.3.15 Try-catch-finally ステートメント
の明確な割り当て分析 試す-キャッチ-ついに 形式のステートメント:
try try-block catch ( ... ) catch-block-1 ... catch ( ... ) catch-block-n finally finally-block
ステートメントが次のように行われるかのように行われます。 試す-ついに を囲むステートメント 試す-キャッチ 声明:
try { try try-block catch ( ... ) catch-block-1 ... catch ( ... ) catch-block-n } finally finally-block
ハフトールさんは本当に好きです ブログ投稿, この議論に 2 セント加えて、データ レイヤーに 1 種類の例外 (DataAccessException) のみをスローさせるのは、私にとって常に簡単だったことを言いたいと思います。このようにして、私のビジネス層はどのような例外が予想されるかを認識し、それをキャッチします。その後、さらなるビジネス ルールに応じて (つまり、ビジネス オブジェクトがワークフローに参加している場合など)、新しい例外 (BusinessObjectException) をスローするか、再スローせずに続行する可能性があります。
必要なときは遠慮せずに try..catch を使用し、賢く使用してください。
たとえば、このメソッドはワークフローに参加します...
コメント?
public bool DeleteGallery(int id)
{
try
{
using (var transaction = new DbTransactionManager())
{
try
{
transaction.BeginTransaction();
_galleryRepository.DeleteGallery(id, transaction);
_galleryRepository.DeletePictures(id, transaction);
FileManager.DeleteAll(id);
transaction.Commit();
}
catch (DataAccessException ex)
{
Logger.Log(ex);
transaction.Rollback();
throw new BusinessObjectException("Cannot delete gallery. Ensure business rules and try again.", ex);
}
}
}
catch (DbTransactionException ex)
{
Logger.Log(ex);
throw new BusinessObjectException("Cannot delete gallery.", ex);
}
return true;
}
Michael L. 著『Programming Languages Pragmatics』で読むことができます。Scott 氏は、最近のコンパイラは一般的な場合、つまり例外が発生しない場合にはオーバーヘッドを追加しないと述べています。したがって、すべての作業はコンパイル時に行われます。ただし、実行時に例外がスローされると、コンパイラーは正しい例外を見つけるためにバイナリ検索を実行する必要があり、これは新しいスローが行われるたびに発生します。
ただし、例外は例外であり、このコストは完全に許容されます。例外を発生させずに例外処理を実行し、代わりにリターン エラー コードを使用しようとすると、おそらくすべてのサブルーチンに if ステートメントが必要になり、これによりリアルタイムのオーバーヘッドが発生します。if ステートメントはいくつかのアセンブリ命令に変換され、サブルーチンに入るたびに実行されることがわかります。
私の英語について申し訳ありませんが、お役に立てれば幸いです。この情報は引用書籍に基づいています。詳細については、第 8.5 章の例外処理を参照してください。
try/catch ブロックを使用する必要のない場所で使用した場合の、考えられる最大のコストの 1 つを分析してみましょう。
int x;
try {
x = int.Parse("1234");
}
catch {
return;
}
// some more code here...
Try/Catch を使用しないものは次のとおりです。
int x;
if (int.TryParse("1234", out x) == false) {
return;
}
// some more code here
わずかな空白を除けば、これら 2 つの同等のコード部分がバイト単位でほぼ同じ長さであることに気づくかもしれません。後者には 4 バイト少ないインデントが含まれます。それは悪いことですか?
さらに追い打ちをかけるように、学生は入力が int として解析できる間にループすることにしました。try/catch を使用しない解決策は次のようになります。
while (int.TryParse(...))
{
...
}
しかし、try/catch を使用すると、これはどうなるでしょうか?
try {
for (;;)
{
x = int.Parse(...);
...
}
}
catch
{
...
}
Try/catch ブロックはインデントを無駄にする魔法の方法ですが、それが失敗した理由はまだわかりません。コードが明らかな例外エラーで停止せずに、重大な論理的欠陥を超えて実行され続けたとき、デバッグを行っている人がどのように感じるかを想像してみてください。Try/catch ブロックは、怠け者のデータ検証/衛生管理です。
より小さなコストの 1 つは、try/catch ブロックが実際に特定の最適化を無効にすることです。 http://msmvps.com/blogs/peterritchie/archive/2007/06/22/performance-implications-of-try-catch-finally.aspx. 。それもプラスの点だと思います。これを使用すると、マルチスレッド アプリケーションの安全で健全なメッセージ パッシング アルゴリズムを機能不全にする可能性がある最適化を無効にしたり、競合状態の可能性をキャッチしたりすることができます ;) try/catch を使用するシナリオは、これが私が思いつく唯一のシナリオです。それにも代替手段はあります。