Sehr oft finde ich mich bei SO dabei, kleine Codestücke zu vergleichen, um festzustellen, welche Implementierung am schnellsten ist.
Sehr oft sehe ich Kommentare, dass der Benchmarking-Code das Jitting oder den Garbage Collector nicht berücksichtigt.
Ich habe die folgende einfache Benchmarking-Funktion, die ich langsam weiterentwickelt habe:
static void Profile(string description, int iterations, Action func) {
// warm up
func();
// clean up
GC.Collect();
var watch = new Stopwatch();
watch.Start();
for (int i = 0; i < iterations; i++) {
func();
}
watch.Stop();
Console.Write(description);
Console.WriteLine(" Time Elapsed {0} ms", watch.ElapsedMilliseconds);
}
Verwendung:
Profile("a descriptions", how_many_iterations_to_run, () =>
{
// ... code being profiled
});
Hat diese Implementierung irgendwelche Mängel? Ist es gut genug zu zeigen, dass die Implementierung X über Z-Iterationen schneller ist als die Implementierung Y? Können Sie sich Möglichkeiten vorstellen, wie Sie dies verbessern können?
BEARBEITEN Es ist ziemlich klar, dass ein zeitbasierter Ansatz (im Gegensatz zu Iterationen) bevorzugt wird. Hat jemand Implementierungen, bei denen die Zeitprüfungen keinen Einfluss auf die Leistung haben?
quelle
Antworten:
Hier ist die geänderte Funktion: Wie von der Community empfohlen, können Sie dies als Community-Wiki ändern.
Stellen Sie sicher , dass Sie in Release mit aktivierten Optimierungen kompilieren und die Tests außerhalb von Visual Studio ausführen . Dieser letzte Teil ist wichtig, da die JIT ihre Optimierungen auch im Release-Modus mit einem angehängten Debugger beendet.
quelle
Die Finalisierung wird nicht unbedingt vor der
GC.Collect
Rücksendung abgeschlossen sein. Die Finalisierung wird in die Warteschlange gestellt und dann in einem separaten Thread ausgeführt. Dieser Thread ist möglicherweise während Ihrer Tests noch aktiv und wirkt sich auf die Ergebnisse aus.Wenn Sie sicherstellen möchten, dass die Finalisierung abgeschlossen ist, bevor Sie mit den Tests beginnen, möchten Sie möglicherweise einen Aufruf durchführen
GC.WaitForPendingFinalizers
, der blockiert wird, bis die Finalisierungswarteschlange gelöscht wird:quelle
GC.Collect()
noch einmal?Collect
ist also da, um sicherzustellen, dass die "finalisierten" Objekte auch gesammelt werden.Wenn Sie GC-Interaktionen aus der Gleichung herausnehmen möchten, möchten Sie möglicherweise Ihren Aufwärmaufruf nach dem GC.Collect-Aufruf ausführen, nicht vorher. Auf diese Weise wissen Sie, dass .NET bereits genügend Speicher vom Betriebssystem für den Arbeitssatz Ihrer Funktion zugewiesen hat.
Denken Sie daran, dass Sie für jede Iteration einen nicht inlinierten Methodenaufruf ausführen. Vergleichen Sie daher die zu testenden Dinge mit einem leeren Körper. Sie müssen auch akzeptieren, dass Sie nur Dinge zuverlässig zeitlich festlegen können, die um ein Vielfaches länger sind als ein Methodenaufruf.
Abhängig davon, welche Art von Material Sie profilieren, möchten Sie Ihr Timing möglicherweise für eine bestimmte Zeit und nicht für eine bestimmte Anzahl von Iterationen ausführen. Dies kann dazu führen, dass es ohne weiteres zu vergleichbareren Zahlen führt für die beste Implementierung eine sehr kurze und / oder für die schlechteste eine sehr lange Laufzeit haben zu müssen.
quelle
Ich würde es überhaupt vermeiden, den Delegierten zu überholen:
Ein Beispielcode, der zur Verwendung von Schließungen führt:
Wenn Ihnen Schließungen nicht bekannt sind, sehen Sie sich diese Methode in .NET Reflector an.
quelle
IDisposable
.Ich denke, das schwierigste Problem, das mit solchen Benchmarking-Methoden zu überwinden ist, besteht darin, Randfälle und das Unerwartete zu berücksichtigen. Zum Beispiel: "Wie funktionieren die beiden Codefragmente bei hoher CPU-Auslastung / Netzwerkauslastung / Festplatten-Thrashing / etc." Sie eignen sich hervorragend für grundlegende Logikprüfungen, um festzustellen, ob ein bestimmter Algorithmus wesentlich schneller als ein anderer funktioniert . Um die meisten Codeleistungen ordnungsgemäß zu testen, müssen Sie jedoch einen Test erstellen, der die spezifischen Engpässe dieses bestimmten Codes misst.
Ich würde immer noch sagen, dass das Testen kleiner Codeblöcke oft nur einen geringen Return on Investment hat und die Verwendung von übermäßig komplexem Code anstelle von einfach wartbarem Code fördern kann. Das Schreiben von klarem Code, den andere Entwickler oder ich 6 Monate später schnell verstehen können, hat mehr Leistungsvorteile als hochoptimierter Code.
quelle
Ich würde
func()
mehrmals zum Aufwärmen anrufen , nicht nur eines.quelle
Vorschläge zur Verbesserung
Erkennen, ob die Ausführungsumgebung für das Benchmarking geeignet ist (z. B. Erkennen, ob ein Debugger angeschlossen ist oder ob die JIT-Optimierung deaktiviert ist, was zu falschen Messungen führen würde).
Teile des Codes unabhängig messen (um genau zu sehen, wo der Engpass liegt).
Zu # 1:
Um festzustellen, ob ein Debugger angehängt ist, lesen Sie die Eigenschaft
System.Diagnostics.Debugger.IsAttached
(Denken Sie daran, auch den Fall zu behandeln, in dem der Debugger anfangs nicht angehängt ist, aber nach einiger Zeit angehängt wird).Lesen Sie die Eigenschaft
DebuggableAttribute.IsJITOptimizerDisabled
der entsprechenden Assemblys , um festzustellen, ob die JIT-Optimierung deaktiviert ist :Zu # 2:
Dies kann auf viele Arten erfolgen. Eine Möglichkeit besteht darin, die Bereitstellung mehrerer Delegaten zuzulassen und diese Delegierten dann einzeln zu messen.
Zu # 3:
Dies könnte auch auf viele Arten geschehen, und unterschiedliche Anwendungsfälle würden sehr unterschiedliche Lösungen erfordern. Wenn der Benchmark manuell aufgerufen wird, ist das Schreiben in die Konsole möglicherweise in Ordnung. Wenn der Benchmark jedoch automatisch vom Build-System ausgeführt wird, ist das Schreiben in die Konsole wahrscheinlich nicht so gut.
Eine Möglichkeit, dies zu tun, besteht darin, das Benchmark-Ergebnis als stark typisiertes Objekt zurückzugeben, das leicht in verschiedenen Kontexten verwendet werden kann.
Etimo.Benchmarks
Ein anderer Ansatz besteht darin, eine vorhandene Komponente zur Durchführung der Benchmarks zu verwenden. Tatsächlich haben wir in meinem Unternehmen beschlossen, unser Benchmark-Tool öffentlich zugänglich zu machen. Im Kern verwaltet es den Garbage Collector, Jitter, Warmups usw., genau wie einige der anderen Antworten hier vorschlagen. Es hat auch die drei Funktionen, die ich oben vorgeschlagen habe. Es verwaltet mehrere der im Eric Lippert-Blog behandelten Themen .
Dies ist eine Beispielausgabe, bei der zwei Komponenten verglichen und die Ergebnisse in die Konsole geschrieben werden. In diesem Fall heißen die beiden verglichenen Komponenten 'KeyedCollection' und 'MultiplyIndexedKeyedCollection':
Es gibt ein NuGet-Paket , ein Beispiel-NuGet-Paket und der Quellcode ist bei GitHub verfügbar . Es gibt auch einen Blog-Beitrag .
Wenn Sie es eilig haben, empfehlen wir Ihnen, das Beispielpaket zu erwerben und die Beispieldelegierten einfach nach Bedarf zu ändern. Wenn Sie es nicht eilig haben, ist es möglicherweise eine gute Idee, den Blog-Beitrag zu lesen, um die Details zu verstehen.
quelle
Sie müssen vor der eigentlichen Messung auch einen "Aufwärm" -Pass ausführen, um die Zeit auszuschließen, die der JIT-Compiler für das Jitting Ihres Codes benötigt.
quelle
Abhängig vom Code, den Sie vergleichen, und der Plattform, auf der er ausgeführt wird, müssen Sie möglicherweise berücksichtigen, wie sich die Code-Ausrichtung auf die Leistung auswirkt . Um dies zu tun, wäre wahrscheinlich ein äußerer Wrapper erforderlich, der den Test mehrmals ausgeführt hat (in separaten App-Domänen oder -Prozessen?). Manchmal wird zuerst "Padding-Code" aufgerufen, um die JIT-Kompilierung zu erzwingen und den Code zu veranlassen Benchmarking anders ausgerichtet. Ein vollständiges Testergebnis würde die besten und schlechtesten Zeitpunkte für die verschiedenen Code-Ausrichtungen liefern.
quelle
Wenn Sie versuchen, die Auswirkungen der Garbage Collection aus dem vollständigen Benchmark zu entfernen, lohnt es sich, diese festzulegen
GCSettings.LatencyMode
?Wenn nicht, und Sie möchten, dass die Auswirkungen des erzeugten Mülls
func
Teil des Benchmarks sind, sollten Sie dann nicht auch die Erfassung am Ende des Tests (innerhalb des Timers) erzwingen?quelle
Das Grundproblem bei Ihrer Frage ist die Annahme, dass eine einzige Messung alle Ihre Fragen beantworten kann. Sie müssen mehrmals messen, um ein effektives Bild der Situation zu erhalten, insbesondere in einer Sprache, in der Müll gesammelt wird, wie z. B. C #.
Eine andere Antwort bietet eine gute Möglichkeit, die Grundleistung zu messen.
Diese einzelne Messung berücksichtigt jedoch nicht die Speicherbereinigung. Ein korrektes Profil berücksichtigt zusätzlich die Worst-Case-Leistung der Garbage Collection, die über viele Anrufe verteilt ist (diese Nummer ist nutzlos, da die VM beendet werden kann, ohne jemals übrig gebliebenen Garbage Collection zu sammeln, aber dennoch nützlich ist, um zwei verschiedene Implementierungen von zu vergleichen
func
.)Möglicherweise möchten Sie auch die Worst-Case-Leistung der Speicherbereinigung für eine Methode messen, die nur einmal aufgerufen wird.
Wichtiger als die Empfehlung spezifischer möglicher zusätzlicher Messungen für das Profil ist jedoch die Idee, dass mehrere verschiedene Statistiken und nicht nur eine Art von Statistik gemessen werden sollten.
quelle