Overhead von try / endlich in C #?

72

Wir haben viele Fragen darüber gesehen, wann und warum try/ catchund try/ catch/ verwendet werden sollen finally. Und ich weiß, dass es definitiv einen Anwendungsfall für try/ gibt finally(zumal die usingAnweisung 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 tryBlock passiert , wie hoch ist der Aufwand, um sicherzustellen, dass die finallyAnweisungen beim Verlassen des tryBlocks 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 DoWithTryFinallyoder 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/ finallyaus 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.Workingist false.

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.Workingin 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.

Platinum Azure
quelle
Ich verstehe die Frage nicht ganz. Sie sammeln immer noch, Sie fangen nur nicht die Ausnahme. Das Rangieren muss doch noch stattfinden, oder?
Jcolebrand
6
Ich denke, der Rest von uns versteht die Frage. Das ist ein guter.
DOK
1
@DOK (+1) ~ Ich dachte, es wäre auch gut, ich glaube, ich habe mir den Haken ausgedacht: Ich denke, das ist das Gleiche wie "Was kostet eine Versicherung, wenn mir nie etwas Schlimmes passiert?" ... Ich gehe davon aus, dass die Kosten anfallen werden, wenn keine Ausnahme ausgelöst wird, aber Sie können sie trotzdem bezahlen.
Jcolebrand
7
Wenn Sie hundert Rückgabepunkte haben, sollten Sie meiner Meinung nach umgestalten :-)
Philipp
@drachenstern: Ja, du hast es im Grunde genommen. In meiner aktualisierten Version finden Sie eine bessere Vorstellung davon, was ich vorhabe.
Platinum Azure

Antworten:

97

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.

Sockel
quelle
Ich bezweifle, dass dies den vorausschauenden Mikroblick optimieren würde. Dies ist nur eine nahezu nutzlose Optimierung, es sei denn, der Anweisungsabruf könnte die Optimierung realisieren und zwei Aufrufe überspringen.
Jcolebrand
54

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 finallyohne bekommen kann finally.

Brian Rasmussen
quelle
8
Ein großartiger zusätzlicher Punkt; Wenn Sie brauchen finally , brauchen Sie es!
Andrew Barber
2
Ich habe meine Frage bearbeitet. Ich frage speziell nach der Verwendung, wenn ich mir keine Sorgen um Ausnahmen mache. Ergo kann ich zwischen verschiedenen Optionen wählen (gemäß Ihrem Punkt in der Antwort). Bitte aktualisieren Sie mit Ihren Gedanken.
Platinum Azure
1
Sie können sie manchmal in einer Schleife weiter nach außen verschieben. Zum Beispiel ist das Delphi-Äquivalent von try / catch oder try / finally ziemlich teuer, also musste ich dies einige Male tun.
CodesInChaos
4
Die beiden Methoden in Ihrer aktualisierten Frage lassen sich nicht wirklich vergleichen, da sie sich im Fehlerfall unterschiedlich verhalten. Ich bin mir bewusst, dass die Methode keine Ausnahme auslösen soll, aber es kann dennoch zu Ausnahmen wie OutOfMemoryException, ThreadAbortException und einigen anderen kommen. In diesem Fall verhalten sich die Methoden anders.
Brian Rasmussen
3
@BrianRasmussen Haben Sie ein Problem mit einer Leistungsfrage? "Ich kann nicht sehen, wie man die Semantik von endlich bekommen kann, ohne endlich zu verwenden" - wer hat diese Frage gestellt? Die eigentliche Frage war: "Overhead of try / endlich in C #?" Ziemlich unschuldig, es sei denn, wir müssen leistungsbezogene Fragen annehmen, selbst bei einfachen, aber niedrigen Themen wie diesen, müssen verpönt werden.
Nicholas Petersen
28

try/finallyist sehr leicht. Eigentlich ist das try/catch/finallyso, 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 ohne try/catch/finally.

Andrew Barber
quelle
Ohne Code / Binärdateien macht dies keinen Sinn. Der Compiler könnte einfach alles in / mono / dev / null optimieren.
Stefan
13

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.

Nicholas Petersen
quelle
6

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

Bengie
quelle
6

In niedrigeren Levels finallyist es genauso teuer wie elsewenn die Bedingung nicht erfüllt ist. Es ist eigentlich ein Sprung in Assembler (IL).

Liviu Mandras
quelle