try
/catch
とtry
/catch
/finally
をいつどのように使用するかについて多くの質問がありました。そして、私はtry
/finally
のユースケースが確かにあることを知っています(特にそれがusing
ステートメントの実装方法であるため)。
try/catchと例外のオーバーヘッド についての質問もあります。
しかし、私がリンクした質問は、JUSTを試してみるオーバーヘッドについては話していません。
try
ブロックで発生することから例外がないと仮定すると、finally
ブロックを離れるときにtry
ステートメントが実行されるようにするオーバーヘッドは何ですか(場合によっては関数から)?
繰り返しになりますが、私はtry
/finally
についてのみ質問し、catch
については質問せず、例外をスローしません。
ありがとう!
編集: さて、ユースケースをもう少しよく見せようと思います。
DoWithTryFinally
とDoWithoutTryFinally
のどちらを使用すればよいですか?
public bool DoWithTryFinally()
{
this.IsBusy = true;
try
{
if (DoLongCheckThatWillNotThrowException())
{
this.DebugLogSuccess();
return true;
}
else
{
this.ErrorLogFailure();
return false;
}
}
finally
{
this.IsBusy = false;
}
}
public bool DoWithoutTryFinally()
{
this.IsBusy = true;
if (DoLongCheckThatWillNotThrowException())
{
this.DebugLogSuccess();
this.IsBusy = false;
return true;
}
else
{
this.ErrorLogFailure();
this.IsBusy = false;
return false;
}
}
このケースは、リターンポイントが2つしかないため、非常に単純ですが、4つ...または10つ...または100個あると想像してください。
ある時点で、次の理由でtry
/finally
を使用したいと思います。
this.Working
がfalse
に設定されていることを確認したいと思います。だから仮説的に、与えられた パフォーマンスの懸念、保守性、およびDRY原則、 出口点の数(特に私が できる すべての内部例外がキャッチされたと仮定します)try
/finally
に関連するパフォーマンスペナルティを発生させたいですか?
編集#2:this.Working
の名前をthis.IsBusy
に変更しました。申し訳ありませんが、これはマルチスレッドであることに言及するのを忘れました(ただし、実際にメソッドを呼び出すのは1つのスレッドだけです)。他のスレッドは、オブジェクトがその作業を行っているかどうかを確認するためにポーリングします。戻り値は、作業が期待どおりに進んだ場合の成功または失敗にすぎません。
実際に得られるものを見てみませんか?
C#のコードの簡単なチャンクは次のとおりです。
static void Main(string[] args)
{
int i = 0;
try
{
i = 1;
Console.WriteLine(i);
return;
}
finally
{
Console.WriteLine("finally.");
}
}
そして、デバッグビルドで結果として得られるILは次のとおりです。
.method private hidebysig static void Main(string[] args) cil managed
{
.entrypoint
.maxstack 1
.locals init ([0] int32 i)
L_0000: nop
L_0001: ldc.i4.0
L_0002: stloc.0
L_0003: nop
L_0004: ldc.i4.1
L_0005: stloc.0
L_0006: ldloc.0 // here's the WriteLine of i
L_0007: call void [mscorlib]System.Console::WriteLine(int32)
L_000c: nop
L_000d: leave.s L_001d // this is the flavor of branch that triggers finally
L_000f: nop
L_0010: ldstr "finally."
L_0015: call void [mscorlib]System.Console::WriteLine(string)
L_001a: nop
L_001b: nop
L_001c: endfinally
L_001d: nop
L_001e: ret
.try L_0003 to L_000f finally handler L_000f to L_001d
}
デバッグで実行するときにJITによって生成されるアセンブリは次のとおりです。
00000000 Push ebp
00000001 mov ebp,esp
00000003 Push edi
00000004 Push esi
00000005 Push ebx
00000006 sub esp,34h
00000009 mov esi,ecx
0000000b lea edi,[ebp-38h]
0000000e mov ecx,0Bh
00000013 xor eax,eax
00000015 rep stos dword ptr es:[edi]
00000017 mov ecx,esi
00000019 xor eax,eax
0000001b mov dword ptr [ebp-1Ch],eax
0000001e mov dword ptr [ebp-3Ch],ecx
00000021 cmp dword ptr ds:[00288D34h],0
00000028 je 0000002F
0000002a call 59439E21
0000002f xor edx,edx
00000031 mov dword ptr [ebp-40h],edx
00000034 nop
int i = 0;
00000035 xor edx,edx
00000037 mov dword ptr [ebp-40h],edx
try
{
0000003a nop
i = 1;
0000003b mov dword ptr [ebp-40h],1
Console.WriteLine(i);
00000042 mov ecx,dword ptr [ebp-40h]
00000045 call 58DB2EA0
0000004a nop
return;
0000004b nop
0000004c mov dword ptr [ebp-20h],0
00000053 mov dword ptr [ebp-1Ch],0FCh
0000005a Push 4E1584h
0000005f jmp 00000061
}
finally
{
00000061 nop
Console.WriteLine("finally.");
00000062 mov ecx,dword ptr ds:[036E2088h]
00000068 call 58DB2DB4
0000006d nop
}
0000006e nop
0000006f pop eax
00000070 jmp eax
00000072 nop
}
00000073 nop
00000074 lea esp,[ebp-0Ch]
00000077 pop ebx
00000078 pop esi
00000079 pop edi
0000007a pop ebp
0000007b ret
0000007c mov dword ptr [ebp-1Ch],0
00000083 jmp 00000072
さて、トライアンドファイナルとリターンをコメントアウトすると、JITからほぼ同じアセンブリを取得します。表示される違いは、finallyブロックへのジャンプと、finallyが実行された後にどこに行くかを理解するためのコードです。だからあなたは小さな違いについて話している。リリースでは、finallyへのジャンプが最適化されます-中括弧はnop命令であるため、これは次の命令へのジャンプになります。これもnopです-これは簡単なのぞき穴最適化です。 popeaxそしてjmpeaxも同様に安いです。
{
00000000 Push ebp
00000001 mov ebp,esp
00000003 Push edi
00000004 Push esi
00000005 Push ebx
00000006 sub esp,34h
00000009 mov esi,ecx
0000000b lea edi,[ebp-38h]
0000000e mov ecx,0Bh
00000013 xor eax,eax
00000015 rep stos dword ptr es:[edi]
00000017 mov ecx,esi
00000019 xor eax,eax
0000001b mov dword ptr [ebp-1Ch],eax
0000001e mov dword ptr [ebp-3Ch],ecx
00000021 cmp dword ptr ds:[00198D34h],0
00000028 je 0000002F
0000002a call 59549E21
0000002f xor edx,edx
00000031 mov dword ptr [ebp-40h],edx
00000034 nop
int i = 0;
00000035 xor edx,edx
00000037 mov dword ptr [ebp-40h],edx
//try
//{
i = 1;
0000003a mov dword ptr [ebp-40h],1
Console.WriteLine(i);
00000041 mov ecx,dword ptr [ebp-40h]
00000044 call 58EC2EA0
00000049 nop
// return;
//}
//finally
//{
Console.WriteLine("finally.");
0000004a mov ecx,dword ptr ds:[034C2088h]
00000050 call 58EC2DB4
00000055 nop
//}
}
00000056 nop
00000057 lea esp,[ebp-0Ch]
0000005a pop ebx
0000005b pop esi
0000005c pop edi
0000005d pop ebp
0000005e ret
つまり、試用/最終的には非常にわずかなコストで済みます。これが問題となる問題領域はほとんどありません。 memcpyのようなことをしていて、コピーされる各バイトの周りにtry/finalを入れてから、数百MBのデータのコピーに進むと、それが問題であることがわかりますが、ほとんどの使用法では?無視できる。
それで、オーバーヘッドがあると仮定しましょう。では、finally
の使用をやめますか?うまくいけない。
IMOパフォーマンスメトリックは、さまざまなオプションから選択できる場合にのみ関連します。 finally
を使用せずにfinally
のセマンティクスを取得する方法がわかりません。
try/finally
は非常に軽量です。実際には、例外がスローされない限り、try/catch/finally
も同様です。
私はそれをテストするために少し前に行ったクイックプロファイルアプリを持っていました。タイトなループでは、実行時間に何も追加されませんでした。
もう一度投稿しますが、とても簡単でした。ループ内で例外をスローしないtry/catch/finally
を使用して、何かを実行するタイトループを実行し、try/catch/finally
のないバージョンに対して結果の時間を計測します。
実際にこれにいくつかのベンチマーク数値を入れてみましょう。このベンチマークが示しているのは、実際、try/finalを実行する時間は、空の関数の呼び出しのオーバーヘッドとほぼ同じくらい短いことです(おそらく、ILの専門家が述べたように、「次の命令へのジャンプ」と言います。上記)。
static void RunTryFinallyTest()
{
int cnt = 10000000;
Console.WriteLine(TryFinallyBenchmarker(cnt, false));
Console.WriteLine(TryFinallyBenchmarker(cnt, false));
Console.WriteLine(TryFinallyBenchmarker(cnt, false));
Console.WriteLine(TryFinallyBenchmarker(cnt, false));
Console.WriteLine(TryFinallyBenchmarker(cnt, false));
Console.WriteLine(TryFinallyBenchmarker(cnt, true));
Console.WriteLine(TryFinallyBenchmarker(cnt, true));
Console.WriteLine(TryFinallyBenchmarker(cnt, true));
Console.WriteLine(TryFinallyBenchmarker(cnt, true));
Console.WriteLine(TryFinallyBenchmarker(cnt, true));
Console.ReadKey();
}
static double TryFinallyBenchmarker(int count, bool useTryFinally)
{
int over1 = count + 1;
int over2 = count + 2;
if (!useTryFinally)
{
var sw = Stopwatch.StartNew();
for (int i = 0; i < count; i++)
{
// do something so optimization doesn't ignore whole loop.
if (i == over1) throw new Exception();
if (i == over2) throw new Exception();
}
return sw.Elapsed.TotalMilliseconds;
}
else
{
var sw = Stopwatch.StartNew();
for (int i = 0; i < count; i++)
{
// do same things, just second in the finally, make sure finally is
// actually doing something and not optimized out
try
{
if (i == over1) throw new Exception();
} finally
{
if (i == over2) throw new Exception();
}
}
return sw.Elapsed.TotalMilliseconds;
}
}
結果:33,33,32,35,32 63,64,69,66,66(ミリ秒、コードの最適化がオンになっていることを確認してください)
したがって、1000万ループでのtry/finallyのオーバーヘッドは約33ミリ秒です。
試行ごと/最後に、0.033/10000000 =
3.3ナノ秒または試行/最終の33億分の1秒のオーバーヘッド。
アンドリュー・バーバーが言ったこと。実際のTRY/CATCHステートメントは、例外がスローされない限り、オーバーヘッドを追加しないか、無視できる程度です。最終的に特別なことは何もありません。 try + catchステートメントのコードが完了した後、コードは常にfinallyにジャンプします。
下位レベルでは、条件が満たされない場合、finally
はelse
と同じくらい高価です。これは実際にはアセンブラ(IL)のジャンプです。