Wir haben viele Fragen darüber gesehen, wann und warum try
/ catch
und try
/ catch
/ verwendet werden sollen finally
. Und ich weiß, dass es definitiv einen Anwendungsfall für try
/ gibt finally
(zumal die using
Anweisung so implementiert wird).
Wir haben auch Fragen zum Overhead von try / catch und Ausnahmen gesehen .
Die Frage, mit der ich verlinkt habe, spricht jedoch nicht über den Aufwand, NUR endlich zu versuchen.
Angenommen, es gibt keine Ausnahmen von allem, was im try
Block passiert , wie hoch ist der Aufwand, um sicherzustellen, dass die finally
Anweisungen beim Verlassen des try
Blocks ausgeführt werden (manchmal durch Rückkehr von der Funktion)?
Wieder frage ich NUR nach try
/ finally
, nein catch
, keine Ausnahmen.
Vielen Dank!
EDIT: Okay, ich werde versuchen, meinen Anwendungsfall ein wenig besser zu zeigen.
Welches soll ich verwenden DoWithTryFinally
oder 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;
}
}
Dieser Fall ist zu simpel, weil es nur zwei Rückgabepunkte gibt, aber stellen Sie sich vor, es wären vier ... oder zehn ... oder hundert.
Irgendwann möchte ich try
/ finally
aus folgenden Gründen verwenden:
- Halten Sie sich an die DRY-Prinzipien (insbesondere wenn die Anzahl der Austrittspunkte höher wird).
- Wenn sich herausstellt, dass ich falsch liege, weil meine innere Funktion keine Ausnahme auslöst, möchte ich sicherstellen, dass auf gesetzt
this.Working
istfalse
.
In Anbetracht der Leistungsbedenken, der Wartbarkeit und der DRY-Prinzipien möchte ich hypothetisch für welche Anzahl von Austrittspunkten (insbesondere wenn ich davon ausgehen kann , dass alle inneren Ausnahmen abgefangen werden) eine Leistungsstrafe erleiden, die mit try
/ verbunden ist finally
.
EDIT # 2: Ich habe den Namen von this.Working
in geändert this.IsBusy
. Entschuldigung, ich habe vergessen zu erwähnen, dass dies Multithread ist (obwohl nur ein Thread jemals die Methode aufrufen wird). Andere Threads fragen ab, ob das Objekt seine Arbeit erledigt. Der Rückgabewert ist lediglich Erfolg oder Misserfolg, wenn die Arbeit wie erwartet verlaufen ist.
quelle
Antworten:
Warum nicht schauen, was Sie tatsächlich bekommen?
Hier ist ein einfacher Codeabschnitt in C #:
static void Main(string[] args) { int i = 0; try { i = 1; Console.WriteLine(i); return; } finally { Console.WriteLine("finally."); } }
Und hier ist die resultierende IL im Debug-Build:
.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 }
und hier ist die Assembly, die von der JIT beim Ausführen im Debug generiert wird:
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
Wenn ich nun den Versuch und schließlich und die Rückgabe auskommentiere, erhalte ich eine nahezu identische Baugruppe von der JIT. Die Unterschiede, die Sie sehen werden, sind ein Sprung in den finally-Block und ein Code, um herauszufinden, wohin Sie gehen müssen, nachdem das finally ausgeführt wurde. Sie sprechen also von winzigen Unterschieden. In der Version wird der Sprung in die endgültige Ausgabe optimiert - Klammern sind keine Anweisungen, daher würde dies ein Sprung zur nächsten Anweisung werden, die auch keine Anweisung ist - das ist eine einfache Gucklochoptimierung. Das pop eax und dann jmp eax ist ähnlich billig.
{ 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
Sie sprechen also von sehr, sehr geringen Kosten für den Versuch / Endlich. Es gibt nur sehr wenige Problembereiche, in denen dies von Bedeutung ist. Wenn Sie so etwas wie memcpy ausführen und jedes zu kopierende Byte versuchen / endgültig umsetzen und dann Hunderte von MB Daten kopieren, könnte ich sehen, dass dies ein Problem ist, aber in den meisten Fällen? Unerheblich.
quelle
Nehmen wir also an, es gibt einen Overhead. Wirst du dann aufhören zu benutzen
finally
? Hoffentlich nicht.IMO-Leistungsmetriken sind nur relevant, wenn Sie zwischen verschiedenen Optionen wählen können. Ich kann nicht sehen, wie man die Semantik von
finally
ohne bekommen kannfinally
.quelle
finally
, brauchen Sie es!try/finally
ist sehr leicht. Eigentlich ist dastry/catch/finally
so, solange keine Ausnahme ausgelöst wird.Ich hatte eine schnelle Profil-App, die ich vor einiger Zeit gemacht habe, um sie zu testen. In einer engen Schleife hat es der Ausführungszeit wirklich nichts hinzugefügt.
Ich würde es wieder posten, aber es war wirklich einfach; Führen Sie einfach eine enge Schleife aus, die etwas tut, mit einer
try/catch/finally
, die keine Ausnahmen innerhalb der Schleife auslöst, und messen Sie die Ergebnisse mit einer Version ohnetry/catch/finally
.quelle
Lassen Sie uns tatsächlich einige Benchmark-Zahlen dazu setzen. Was dieser Benchmark zeigt, ist, dass die Zeit für einen Versuch / Endlich ungefähr so kurz ist wie der Aufwand für einen Aufruf einer leeren Funktion (wahrscheinlich besser ausgedrückt: "Ein Sprung zur nächsten Anweisung", wie der IL-Experte es ausdrückte über).
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; } }
Ergebnis: 33,33,32,35,32 63,64,69,66,66 (Millisekunden, stellen Sie sicher, dass Sie die Codeoptimierung aktiviert haben)
Also ungefähr 33 Millisekunden Overhead für den Versuch / schließlich in 10 Millionen Schleifen.
Pro Versuch / schließlich sprechen wir von 0,033 / 10000000 =
3,3 Nanosekunden oder 3,3 Milliardstel Sekunden Overhead eines Versuchs / Endlich.
quelle
Was Andrew Barber gesagt hat. Die tatsächlichen TRY / CATCH-Anweisungen verursachen keinen / vernachlässigbaren Overhead, es sei denn, eine Ausnahme wird ausgelöst. Endlich gibt es nichts Besonderes. Ihr Code springt einfach immer zu, nachdem der Code in den try + catch-Anweisungen fertig ist
quelle
In niedrigeren Levels
finally
ist es genauso teuer wieelse
wenn die Bedingung nicht erfüllt ist. Es ist eigentlich ein Sprung in Assembler (IL).quelle