Einleitung: Warum ist Leistung von Anfang an wichtig?
\nJeder C#-Entwickler stößt früher oder später auf die Situation, dass die Anwendung zu trödeln beginnt. Oft werden Leistungsprobleme in frühen Phasen verursacht – durch suboptimale Algorithmen, falsche Speicherverwaltung oder übermäßige Nutzung von LINQ ohne Verständnis seiner internen Funktionsweise. Für Anfänger mag dieses Thema komplex erscheinen, aber tatsächlich sind die grundlegenden Optimierungsprinzipien recht einfach und können in wenigen Stunden Praxis erlernt werden.
\nIn diesem Artikel werden wir vier Schlüsselaspekte behandeln, die Ihnen helfen, schnelle und effiziente C#-Anwendungen auf der .NET-Plattform zu schreiben. Sie erfahren, wie Sie Code profilieren, LINQ-Abfragen optimieren, Speicher richtig verwalten und Asynchronität nutzen. Die Codebeispiele werden so praxisnah wie möglich an realen Aufgaben sein, mit denen Entwickler konfrontiert werden.
\n\n1. Profiling: Der erste Schritt zur Optimierung
\nBevor Sie etwas optimieren, müssen Sie verstehen, was genau trödelt. Intuitive Vermutungen sind oft falsch. Profiling ist der Prozess der Messung der Leistung Ihres Codes mit speziellen Werkzeugen.
\n\nWerkzeuge für das Profiling
\n- \n
- Visual Studio Diagnostic Tools – integriertes Werkzeug, verfügbar in den Editionen Community und höher. Ermöglicht die Verfolgung von CPU-, Speicher- und Ausführungszeitnutzung. \n
- JetBrains dotMemory / dotTrace – leistungsstarke kommerzielle Dienstprogramme zur Analyse von Speicher und Ausführungszeit. \n
- BenchmarkDotNet – eine leichtgewichtige Bibliothek für Mikro-Benchmarking. Ideal zum Vergleich zweier Implementierungen derselben Methode. \n
Beispiel: Messung der Ausführungszeit
\nVerwenden Sie Stopwatch für eine einfache Messung:
using System.Diagnostics;\n\nvar stopwatch = Stopwatch.StartNew();\n// Ihr Code\nfor (int i = 0; i < 1000000; i++)\n{\n // Eine Operation\n}\nstopwatch.Stop();\nConsole.WriteLine($"Ausführungszeit: {stopwatch.ElapsedMilliseconds} ms");\nTipp: Verwenden Sie für genaue Messungen BenchmarkDotNet, da Stopwatch den Overhead der JIT-Kompilierung nicht berücksichtigt.
2. Arbeit mit Speicher und Zeichenketten
\nDie Speicherverwaltung in .NET ist automatisiert (Garbage Collector), aber das bedeutet nicht, dass man sie vergessen kann. Eines der häufigsten Probleme ist die übermäßige Erstellung von Zeichenketten.
\n\nProblem: Zeichenkettenverkettung in einer Schleife
\nZeichenketten in C# sind unveränderlich. Jede +-Operation erstellt eine neue Zeichenkette, was zu ständigen Speicherzuweisungen und Druck auf den GC führt:
string result = "";\nfor (int i = 0; i < 10000; i++)\n{\n result += i.ToString(); // SCHLECHT: erstellt 10000 neue Zeichenketten\n}\n\nLösung: StringBuilder
\nDie Klasse StringBuilder arbeitet mit einem Zeichenpuffer und minimiert Speicherzuweisungen:
var sb = new StringBuilder();\nfor (int i = 0; i < 10000; i++)\n{\n sb.Append(i);\n}\nstring result = sb.ToString(); // GUT: eine Speicherzuweisung am Ende\n\nBoxing vermeiden
\nBoxing (Verpacken von Werttypen in ein Objekt) ist eine weitere Quelle versteckter Speicherzuweisungen. Beispiel:
\nArrayList list = new ArrayList();\nlist.Add(42); // Boxing: int wird in object verpackt\n\n// Besser generische Sammlungen verwenden:\nList<int> list2 = new List<int>();\nlist2.Add(42); // Ohne Boxing\n\n3. Optimierung von LINQ-Abfragen
\nLINQ ist ein mächtiges Werkzeug, aber seine falsche Verwendung kann die Anwendung stark verlangsamen. Das Hauptproblem sind mehrfache Durchläufe von Sammlungen.
\n\nProblem: Mehrfache Iterationen
\nJeder Aufruf von Where(), Select(), Count() kann die Sammlung erneut durchlaufen, wenn das Ergebnis nicht materialisiert wird:
var numbers = Enumerable.Range(1, 1000000);\nvar evenNumbers = numbers.Where(n => n % 2 == 0);\nint coun