2009-08-28 5 views
34

Ich erwäge, etwas wie StackFrame stackFrame = new StackFrame(1) zu verwenden, um die ausführende Methode zu protokollieren, aber ich weiß nicht über seine Auswirkungen auf die Leistung. Ist der Stack-Trace etwas, das bei jedem Methodenaufruf ohnehin erstellt wird, sodass die Performance keine Rolle spielt oder nur auf Nachfrage erstellt wird? Empfehlen Sie dagegen in einer Anwendung, in der die Leistung sehr wichtig ist? Wenn ja, heißt das, ich sollte es für die Veröffentlichung deaktivieren?Wie leistungsfähig ist StackFrame?

+3

Vorsicht, es gibt einen SO-Bot, der alle Fragen beantwortet: Probieren Sie es aus! – harpo

+0

Wird diese Protokollierung immer aktiviert? Was ist der alternative Ansatz? Michaels Ergebnisse sind interessant, aber die eigentliche Frage ist: "Wie kommt dieser Ansatz zu X?" – STW

+0

@Yoooder: gute Fragen. Ich denke, das "X", mit dem wir das vergleichen, ist die Protokollierung ohne Methodeninformationen oder mit statisch generierten Methodeninformationen. "Sollte ich es deaktivieren?": Testen Sie in Ihrer Anwendung, und sehen Sie, ob die Leistungsunterschiede messbar oder bemerkbar sind. –

Antwort

28

edit: Einige Hintergrund


Wir haben eine ähnliche Funktion haben, die 99% der Zeit deaktiviert ist; wir wurden mit einem Ansatz wie:

public void DoSomething() 
{ 
    TraceCall(MethodBase.GetCurrentMethod().Name); 
    // Do Something 
} 

public void TraceCall(string methodName) 
{ 
    if (!loggingEnabled) { return; } 
    // Log... 
} 

TraceCall(MethodBase.GetCurrentMethod().Name) 

Es war einfach, aber unabhängig davon, ob oder nicht, wurde Tracing aktiviert wir die Performance-Einbußen Reflection wurden entstehen für die Verwendung der Methode Namen nachzuschlagen.

waren unsere Optionen entweder zu mehr Code in jeder Methode (und Risiko einfache Fehler oder Verweigerung) oder wechseln zu verwenden StackFrame erfordern die Rufmethode nur wenn die Protokollierung aktiviert wurde, um zu bestimmen.

Option A:

public void DoSomething() 
{ 
    if (loggingEnabled) 
    { 
     TraceCall(MethodBase.GetCurrentMethod().Name); 
    } 
    // Do Something 
} 

public void TraceCall(string methodName) 
{ 
    if (!loggingEnabled) { return; } 
    // Log... 
} 

Option B:

public void DoSomething() 
{ 
    TraceCall(); 
    // Do Something 
} 

public void TraceCall() 
{ 
    if (!loggingEnabled) { return; } 
    StackFrame stackFrame = new StackFrame(1); 
    // Log... 
} 

Wir für Option B entschieden es erhebliche Leistungsverbesserungen gegenüber Option bietet ein bei der Anmeldung deaktiviert ist, 99% der Zeit und ist sehr einfach zu implementieren.

Hier ist eine Änderung von Michaels Code,

die Kosten/Nutzen dieses Ansatzes anzuzeigen
using System; 
using System.Diagnostics; 
using System.Reflection; 

namespace ConsoleApplication 
{ 
    class Program 
    { 
     static bool traceCalls; 

     static void Main(string[] args) 
     { 
      Stopwatch sw; 

      // warm up 
      for (int i = 0; i < 100000; i++) 
      { 
       TraceCall(); 
      } 

      // call 100K times, tracing *disabled*, passing method name 
      sw = Stopwatch.StartNew(); 
      traceCalls = false; 
      for (int i = 0; i < 100000; i++) 
      { 
       TraceCall(MethodBase.GetCurrentMethod()); 
      } 
      sw.Stop(); 
      Console.WriteLine("Tracing Disabled, passing Method Name: {0}ms" 
          , sw.ElapsedMilliseconds); 

      // call 100K times, tracing *enabled*, passing method name 
      sw = Stopwatch.StartNew(); 
      traceCalls = true; 
      for (int i = 0; i < 100000; i++) 
      { 
       TraceCall(MethodBase.GetCurrentMethod()); 
      } 
      sw.Stop(); 
      Console.WriteLine("Tracing Enabled, passing Method Name: {0}ms" 
          , sw.ElapsedMilliseconds); 

      // call 100K times, tracing *disabled*, determining method name 
      sw = Stopwatch.StartNew(); 
      traceCalls = false; 
      for (int i = 0; i < 100000; i++) 
      { 
       TraceCall(); 
      } 
      Console.WriteLine("Tracing Disabled, looking up Method Name: {0}ms" 
         , sw.ElapsedMilliseconds); 

      // call 100K times, tracing *enabled*, determining method name 
      sw = Stopwatch.StartNew(); 
      traceCalls = true; 
      for (int i = 0; i < 100000; i++) 
      { 
       TraceCall(); 
      } 
      Console.WriteLine("Tracing Enabled, looking up Method Name: {0}ms" 
         , sw.ElapsedMilliseconds); 

      Console.ReadKey(); 
     } 

     private static void TraceCall() 
     { 
      if (traceCalls) 
      { 
       StackFrame stackFrame = new StackFrame(1); 
       TraceCall(stackFrame.GetMethod().Name); 
      } 
     } 

     private static void TraceCall(MethodBase method) 
     { 
      if (traceCalls) 
      { 
       TraceCall(method.Name); 
      } 
     } 

     private static void TraceCall(string methodName) 
     { 
      // Write to log 
     } 
    } 
} 

Die Ergebnisse:

Tracing Disabled, passing Method Name: 294ms 
Tracing Enabled, passing Method Name: 298ms 
Tracing Disabled, looking up Method Name: 0ms 
Tracing Enabled, looking up Method Name: 1230ms 
+0

Ausgezeichneter Punkt: Oft können Sie einen Schritt zurückgehen und einen anderen Weg finden, um das Problem zu lösen. new StackFrame() ist nicht teuer, wenn Sie es nie aufrufen müssen. –

+0

@Michael: genau! Es ist sehr sauber zu implementieren, einfach zu testen, und das einzige Mal, wenn wir den Performance-Hit machen, ist, wenn wir im Wesentlichen debuggen und die Performance sowieso keine Rolle spielt. – STW

+0

Ich denke, es hängt davon ab, wie oft Sie die Ablaufverfolgung aktiviert wird, da Option B immer noch 10x langsamer ist als Option A, wenn die Ablaufverfolgung aktiviert ist. Aber jetzt kann jeder aufgrund von Fakten selbst entscheiden. Sehr aufschlussreich. Vielen Dank! –

5

Aus der MSDN-Dokumentation, wie es scheint Stackframes ganze Zeit erstellt werden:

A StackFrame- erstellt und Aufschieben den Call-Stack für jeden Funktionsaufruf während der Ausführung eines Threads gemacht. Der Stapelrahmen enthält immer MethodBase-Informationen und optional enthält Dateiname, Zeilennummer und Spalte Nummer Informationen.

Der Konstruktor new StackFrame(1) Sie dies tun würde, nennen würde:

private void BuildStackFrame(int skipFrames, bool fNeedFileInfo) 
{ 
    StackFrameHelper sfh = new StackFrameHelper(fNeedFileInfo, null); 
    StackTrace.GetStackFramesInternal(sfh, 0, null); 
    int numberOfFrames = sfh.GetNumberOfFrames(); 
    skipFrames += StackTrace.CalculateFramesToSkip(sfh, numberOfFrames); 
    if ((numberOfFrames - skipFrames) > 0) 
    { 
     this.method = sfh.GetMethodBase(skipFrames); 
     this.offset = sfh.GetOffset(skipFrames); 
     this.ILOffset = sfh.GetILOffset(skipFrames); 
     if (fNeedFileInfo) 
     { 
      this.strFileName = sfh.GetFilename(skipFrames); 
      this.iLineNumber = sfh.GetLineNumber(skipFrames); 
      this.iColumnNumber = sfh.GetColumnNumber(skipFrames); 
     } 
    } 
} 

GetStackFramesInternal ist ein externes Verfahren. CalculateFramesToSkip hat eine Schleife, die genau einmal funktioniert, da Sie nur 1 Rahmen angegeben haben. Alles andere sieht ziemlich schnell aus.

Haben Sie versucht zu messen, wie lange es dauern würde, sagen wir, 1 Million von ihnen?

+0

Danke Paul. Ich habe den gleichen Absatz in MSDN gelesen und versucht, herauszufinden, wie es in Reflector funktioniert, habe es aber nicht wirklich verstanden. Ich habe darüber nachgedacht, es zu times, aber es könnte irreführend sein, wenn es nach dem ersten Anruf irgendwie zwischengespeichert wird, deshalb hoffte ich, dass jemand wusste, wie es intern funktionierte. –

+3

Michaels Test schlägt vor, dass die Stack-Frames nicht für jeden Funktionsaufruf, der während der Ausführung eines Threads ausgeführt wird, "erstellt und auf den Aufruf-Stack geschoben werden", da es länger dauert, danach zu fragen. –

+4

Oder sie können sie anders erstellen, oder es kann ein natives Konstrukt sein, bis angefordert, oder die Dokumente können falsch sein. Ich nehme an, die CLR muss den verwalteten Stack im Auge behalten, daher muss im nativen Speicher-Setup * etwas * sein, wenn der Stack wächst. –

16

Ein schneller und naiver Test zeigt an, dass für die erfolgskritischen Code, ja, Sie darauf aufmerksam bezahlen:

nicht generiert 100K Rahmen: 3 ms

generieren 100K Rahmen: 1805ms

Etwa 20 Mikrosekunden pro generierten Rahmen auf meinem Computer. Nicht viel, aber ein messbarer Unterschied über eine große Anzahl von Iterationen.

Mit Ihren späteren Fragen ("Sollte ich StackFrame-Generierung in meiner Anwendung deaktivieren?"), Würde ich vorschlagen, analysieren Sie Ihre Anwendung, machen Sie Leistungstests wie die, die ich hier getan habe, und sehen Sie, ob der Leistungsunterschied ergibt sich alles mit Ihrer Arbeitsbelastung.

using System; 
using System.Diagnostics; 

namespace ConsoleApplication 
{ 
    class Program 
    { 
     static bool generateFrame; 

     static void Main(string[] args) 
     { 
      Stopwatch sw; 

      // warm up 
      for (int i = 0; i < 100000; i++) 
      { 
       CallA(); 
      } 

      // call 100K times; no stackframes 
      sw = Stopwatch.StartNew(); 
      for (int i = 0; i < 100000; i++) 
      { 
       CallA(); 
      } 
      sw.Stop(); 
      Console.WriteLine("Don't generate 100K frames: {0}ms" 
           , sw.ElapsedMilliseconds); 

      // call 100K times; generate stackframes 
      generateFrame = true; 
      sw = Stopwatch.StartNew(); 
      for (int i = 0; i < 100000; i++) 
      { 
       CallA(); 
      } 
      Console.WriteLine("Generate 100K frames: {0}ms" 
          , sw.ElapsedMilliseconds); 

      Console.ReadKey(); 
     } 

     private static void CallA() 
     { 
      CallB(); 
     } 

     private static void CallB() 
     { 
      CallC(); 
     } 

     private static void CallC() 
     { 
      if (generateFrame) 
      { 
       StackFrame stackFrame = new StackFrame(1); 
      } 
     } 
    } 
} 
+0

Danke Michael! Ich mag die Art und Weise, wie Sie das getestet haben, seit ich mich um die.NET Framework irgendwie die Methode zwischenspeichern, aber Ihr Test scheint zu zeigen, dass dies nicht der Fall ist. –

+0

Ja, ich bin mir nicht sicher, wie man den Artikel, auf den Paul sich bezieht, mit meinen Erfahrungen und Tests vergleicht. Ich bin auch durch die log4net-Dokumentation gegangen, die explizit aufruft, wie langsam generierende Stackframes (für die Aufnahme in Logs) sind. –

+0

Ich wiederholte diesen Test mit [MethodImpl (MethodImplOptions.NoInlining)] auf jede Methode nur für den Fall und ich bekomme auch etwa 20μs für jeden StackFrame mit 100k Iterationen und etwa 15μs für jeden StackFrame mit 1 Million Iterationen. 15-20 Mikrosekunden scheint nicht so schlecht. 50 StackFrame-Kreationen benötigen immerhin nur 1 Millisekunde. –

4

Ich erwäge mit so etwas wie StackFrame- StackFrame- = new StackFrame- (1) zum Protokollieren der Ausführungsmethode

Aus Interesse: Warum? Wenn Sie nur die aktuelle Methode verwenden möchten, dann scheint

besser zu sein. Vielleicht nicht leistungsfähiger (ich habe nicht verglichen, aber Reflection zeigt, dass GetCurrentMethod() nicht einfach einen StackFrame erstellt, sondern "Magie" ausführt), aber klarer in seiner Absicht.

+1

Ich versuchte Michaels Test mit MethodBase.GetCurrentMethod() und du hast Recht, es ist tatsächlich schneller. Dauert etwa 1 Mikrosekunde pro Anruf auf meinem Rechner. –

+0

Ich habe gerade yoooders Antwort gesehen, er macht einen guten Punkt. –

2

Ich denke Paul Williams traf den Nagel auf den Kopf mit der Arbeit, die getan wird. Wenn Sie sich tiefer in StackFrameHelper vertiefen, werden Sie feststellen, dass fNeedFileInfo eigentlich der Performance-Killer ist - besonders im Debug-Modus. Versuchen Sie es false einzustellen, wenn Leistung wichtig ist. Im Freigabemodus werden Sie sowieso nicht viele nützliche Informationen erhalten.

Wenn Sie falsch hier passieren werden Sie noch Methodennamen erhalten, wenn ein ToString() tun und ohne Informationen ausgibt, nur Stapelzeiger bewegen, es ist sehr schnell.

1

Ich weiß, was Sie meinen, aber dieses Beispiel Ergebnis ist ein Überschwingen. Das Ausführen der GetCurrentMethod auch bei deaktivierter Protokollierung ist eine Verschwendung. Es sollte so etwas wie sein:

if (loggingEnabled) TraceCall(MethodBase.GetCurrentMethod()); 

Oder wenn Sie das TraceCall wollen immer ausgeführt:

TraceCall(loggingEnabled ? MethodBase.GetCurrentMethod() : null); 
10

Ich weiß, dass dies eine alte Post, aber nur für den Fall jemand kommt über sie es eine andere Alternative ist, Wenn Sie auf .Net 4.5 zielen.

Sie können das CallerMemberName-Attribut verwenden, um den Namen der aufrufenden Methode zu identifizieren. Es ist viel schneller als Reflection oder StackFrame. Hier sind die Ergebnisse eines millionenfach iterierten Schnelltests. StackFrame ist im Vergleich zur Reflexion extrem langsam und das neue Attribut lässt beide aussehen, als stünden sie still. Dies wurde in der IDE ausgeführt.

Reflexion Ergebnis: 00: 00: 01,4098808

StackFrame- Ergebnis 00.00.06.Attribut 2002501

CallerMemberName Ergebnis: 00: 00: 00,0042708

Getan

Das Folgende ist aus den kompilierten exe: Reflection Ergebnis: 00: 00: 01,2136738 Stackframe Ergebnis 00: 00: 03,6343924 CallerMemberName Attribut Ergebnis: 00: 00: 00.0000947 Geschehen

 static void Main(string[] args) 
    { 

     Stopwatch sw = new Stopwatch(); 

     sw.Stop(); 

     Console.WriteLine("Reflection Result:"); 

     sw.Start(); 
     for (int i = 0; i < 1000000; i++) 
     { 
      //Using reflection to get the current method name. 
      PassedName(MethodBase.GetCurrentMethod().Name); 
     } 
     Console.WriteLine(sw.Elapsed); 

     Console.WriteLine("StackFrame Result"); 

     sw.Restart(); 

     for (int i = 0; i < 1000000; i++) 
     { 
      UsingStackFrame(); 
     } 

     Console.WriteLine(sw.Elapsed); 

     Console.WriteLine("CallerMemberName attribute Result:"); 

     sw.Restart(); 
     for (int i = 0; i < 1000000; i++) 
     { 
      UsingCallerAttribute(); 
     } 

     Console.WriteLine(sw.Elapsed); 

     sw.Stop(); 



     Console.WriteLine("Done"); 
     Console.Read(); 
    } 


    static void PassedName(string name) 
    { 

    } 

    static void UsingStackFrame() 
    { 
     string name = new StackFrame(1).GetMethod().Name; 
    } 


    static void UsingCallerAttribute([CallerMemberName] string memberName = "") 
    { 

    } 
+0

Der Grund, warum dieses Attribut schneller ist, liegt darin, dass der Compiler das tatsächlich direkt in das resultierende Bild einbrennt. An allen Stellen, an denen eine Methode aufgerufen wird, die die Compiler-Services-Attribute verwendet, werden die Werte durch den vom Compiler bereitgestellten Code ersetzt (CallerMemberName, CallerFileName, CallerLineNumber). Es ist im Grunde binäre Umschreiben. Dies kann mit dem ILDASM-Tool bestätigt werden. – PSGuy

+0

Dies erhält auch nicht den Typ des Aufrufers, der offensichtlich nur über einen Stapelrahmen verfügbar ist, und ein wesentlicher Teil davon, herauszufinden, woher etwas kam. – StingyJack

Verwandte Themen