Ich habe lange überlegt, ob ich zu diesem Thema etwas schreibe, schließlich gibt es schon 100er von Blogs, die das Thema behandeln. Letztendlich habe ich mich doch dazu entschlossen, um einfach mal eine kleine Link-Sammlung zu dem Thema zu haben.
Den Post habe ich Anfang Mai angefangen, in der Zwischenzeit ist es schon fast Juli. Das Thema ist so umfangreich und jeden Tag kommt mehr zu dem Thema. Das letzte ist die Google Initiative “Let's make the web faster”. Das Thema passt zum Glück aber nicht ganz in diesen Teil rein. Eigentlich brauche ich zum Thema Web-App-Performance nichts mehr schreiben, Google hat alles zusammengetragen, was dazu gehört.
Bevor mit der Optimierung einer Anwendung begonnen wird, sollte man sich Ziele überlegen, die zuerreichen sind. Das soll einfach dazu dienen, dass man nicht das Projektbudget unnötig verbraucht. Außerdem sollte man unbedingt man die wesentlichen Kapitel im “Improving .NET Application Performance and Scalability”-Guide lesen, im Kapitel “Performance Best Practices at a Glance” geht es um das allgemeine Vorgehen zur Optimierung von Anwendungen. Mindestens ebenso Hilfreich ist der Pattern & Practice Guidance Explorer, hier sind viele Guides, Checklisten, …. Es ist sehr schwer Anwendungen zu optimieren, wenn das Anwendungsdesign nicht schon viele Prinzipien berücksichtigt hat. Dennoch lässt sich meistens einiges herausholen, allerdings sollte man vor jeder Optimierung MESSEN, anschließend die Optimierung durchführen und erneut Messen. Ach und natürlich die Unit-Tests erneut ausführen, um nicht ein fehlerhaftes Verhalten als Optimierung zu bezeichnen.
Ich hatte dievor einigen Wochen einige interessante Blog-Artikel zu Assemblies und Namespaces gelesen. Die meisten Verbinden mit Komponenten separate Assemblies, allerdings sollte jedem bewußtsein, dass mit zusätzlichen Assemblies die Ladezeiten steigen. (Sicherheitscheck, Auflösungen, …) Hier einer der Links: Advices on partitioning code through .NET assemblies - Patrick Smacchia
Im Blog von Rico Mariani kann man sehr viele nützliche Tips zu Performance finden, wer sich mit dem Thema beschäftigt, sollte sich das mal anschauen.
Loops
Jede Anwendung verfügt über Schleifen, zumindest kenne ich keine außer “Hello World” ohne Schleifen. In Abhängigkeit von der Anzahl und Häufigkeit der Schleifendruchläufe kann hier eine Optimierung der Anwendung auf die Sprünge helfen. In dem Blog Post von Patrick Smacchia “An easy and efficient way to improve net code performance” geht es auch genau um das Thema Loops und deren Einfluss auf die Performance. Für die Performance sind For-Schleifen günstiger, vor allem auf Array oder Listen. Foreach-Schleifen sind durch den IEnumerator/IEnumerable-Aufwand teurer. Hier muss bei jedem durchlauf IEnumerator.MoveNext und IEnumerator.Current aufgerufen werden.
Beim Refactoring von Schleifen sollte man zusätzliche darauf achten, dass nicht der Inhalt der Schleife in eine neue Methode gepackt wird, sondern ebenso die Schleife. Der Overhead, der beim Aurfuf einer Methode entsteht, multipliziert sich in einer Schleife. Mein folgendes Beispiel ist nicht sehr sinnvoll, aber verdeutlicht hoffentlich das Problem.
Ausgang:
1: private void Loop()
2: {
3: long sum = 1;
4: for (int i = 0; i < 10000; i++)
5: {
6: sum = i*2;
7: sum -= i;
8: }
9: Debug.WriteLine("Resut=" + sum);
10: }
Ungünstige Optimierung:
1: private void Loop()
2: {
3: long sum = 1;
4: for (int i = 0; i < 10000; i++)
5: {
6: sum = GetSum(i);
7: }
8: Debug.WriteLine("Resut=" + sum);
9: }
10: private long GetSum(int i)
11: {
12: long sum;
13: sum = i*2;
14: sum -= i;
15: return sum;
16: }
Besser:
1: private void Loop()
2: {
3: long sum = 1;
4: sum = GetSum(sum);
5: Debug.WriteLine("Resut=" + sum);
6: }
7: private long GetSum(long sum)
8: {
9: for (int i = 0; i < 10000; i++)
10: {
11: sum = i*2;
12: sum -= i;
13: }
14: return sum;
15: }
Hier noch ein Post zur Laufzeit von verschiedenen Loops.
Casting
Dank Generics kann man das “Cast”en sein lassen. Ich casts sind nicht merkbar “teuer”, das Boxing von ValueTypes kostet einige CPU-Cycles. Allerdings kann das Boxing zu Laufzeitfehlern führen, die der Compiler mittels Generics bereits hätte prüfen könne. Ich verwende massig Generics, allerdings finde ich die Regel keine Generics als Public Properties oder Rückgaben zu verwenden, BLÖD.
Threading
… ist eins meiner Lieblingsthemen, leider kommt dieses Thema aber immer zu kurz und bei der Leistungsoptimierung von Anwendungen wird es oft verkannt. Durch den Einsatz von Threading kann man sich viele Probleme in einer Anwendung schaffen, zu dem kann nicht jede Anwendung mit Threading sinnvoll ausgestattet werden. Um die Probleme mit dem Threading zu reduzieren/vereinfachen, gibt es von Microsoft die Parallel Extensions. Es ist so einfach damit Anwendungen zu Parallelisieren und das auch noch sehr performant. Die Parallel Extensions machen den Rechner nicht mit Threads zu, sondern verfügen über ein intelligentes Thread-Management bei dem eine Anzahl Arbeitsthreads, abhängig vom Prozessor, immer wieder mit den eigentlichen Aufgaben gefüllt wird. Einen Nachteil haben die Parallel Extensions allerdings, es gibt sie nur für Framework 4.0 und für das Framework 3.5 leider nur als CTP.
Eine andere Art Threading effektiv einzusetzen, ist System.Threading.ThreadPool.QueueUserWorkItem. Besonders effektiv ist der Einsatz in Client-Anwendungen, um das User-Interface während langen Operationen verfügbar zu belassen “Responsiveness”. Für Server-Anwendungen muss man etwas aufpassen bei dieser Variante, da der Standard .NET-ThreadPool auch für ASP.NET verwendet wird (evtl. auch WCF?), somit kann es passieren, dass der Server Anfragen ablehnt, weil zu viele Nutzertasks aktiv sind. Es gibt sogar ein schlimmeres Szenario, wenn der Server selber Web-Anfragen ausführt, dann kann es zu Deadlocks kommen. Also dabei etwas aufpassen.
Linq 2 Sql
Compiled Queries sind in Linq 2 Sql der weg um Abfragen zu beschleunigen, dabei wird bei der ersten Ausführung der Anfrage diese in .NET Code generiert und das Füllen der Objekte bei Folgeanfragen deutlich beschleunigt. Von mir gibt es dazu aber kein Sample-Code, es gibt dazu schon massig Infos im Web. Hier nur 2 ausgewählte Adressen:
LINQ to SQL - compiled queries
Solving common problems with Compiled Queries in Linq to Sql for high demand ASP.NET websites
In meinem aktuellen Projekt konnten wir durch den Einsatz von Compiled Queries an gezielten Stellen richtig viel sparen. Ich kann die gesparte Zeit nicht quantifizieren, aber beim Profilen hat man deutlich gesehen, dass alle Datenabrufe um ein vielfaches schneller waren.
Network IO
Ein massgeblicher Einflussfaktor bei vielen modernen Anwendungen ist der Netzwerkverkehr. Grundsätzlich gibt es 2 Regeln für den Netzwerkverkehr
- so wenig wie möglich an Daten transportieren
- möglichst viel mit einem Rutsch, wenige Roundtrips (Chatty Interfaces)
Grundsätzlich sollte man bei allen Netzwerkstrecken prüfen, ob nicht die Datenmenge mittels Komprimierung reduziert und so effektiver übertragen werden kann.
Im IIS 6 kann die eingebaute Komprimeriung mittels der folgenden Zeilen aktiviert werden:
1: cscript c:\Inetpub\AdminScripts\adsutil.vbs set w3svc/filters/compression/parameters/HcDoDynamicCompression true
2: cscript c:\Inetpub\AdminScripts\adsutil.vbs set w3svc/filters/compression/GZIP/HcScriptFileExtensions asmx dll asp aspx ashx exe svc
3: cscript c:\Inetpub\AdminScripts\adsutil.vbs set w3svc/filters/compression/Deflate/HcScriptFileExtensions asmx dll asp aspx ashx exe svc
4: cscript c:\Inetpub\AdminScripts\adsutil.vbs set w3svc/filters/compression/parameters/HcDoStaticCompression true
5: cscript c:\Inetpub\AdminScripts\adsutil.vbs set w3svc/filters/compression/GZIP/HcFileExtensions html htm css js xml xslt xsd log
6: cscript c:\Inetpub\AdminScripts\adsutil.vbs set w3svc/filters/compression/Deflate/HcFileExtensions htm css js xml xslt xsd log
7: iisreset
Wahrscheinlich funktionieren die Zeilen auch im IIS7, das habe ich allerdings noch nicht geprüft. Hier kann die Komprimierung auf jeden Fall leicht auch per Webinterface aktiviert werden.
Für WCF sollte man sich mal das WCF Extension Projekt auf Codeplex ansehen.
WCF Services
WCF ist schon eine Weile mein Nummer eins Thema. Es macht jedes mal Spass mit WCF zu spielen, klar gibt es öfter auch mal Wutanfälle, meistens ist nicht WCF schuld. Eine Sache, bei der WCF einfach schwierig ist, ist das saubere Abräumen/Aufräumen. In WCF-Service Proxies verhält sich die Dispose-Methode nicht, wie in anderen IDisposable-Komponenten, WCF-Service Proxies werfen beim Dispose eine Exception, wenn der Client im Faulted-State ist. Daher sollte man nicht das Using-Pattern implementieren, sondern sollte den Proxy gezielt schließen, wenn es zu einem Fehler kommt, dann sollte man die Kommunikation mittels Abort() abrechen. Eine nette Implementierung für dieses Problem kann man bei Erwyn van der Meer “WCF Service Proxy Helper” sehen. (weiterer Artikel zum Disposing: Disposing a WCF Proxy)
Ich bin ein absoluter verfechter der strikten Entities, damit meine ich alle WCF-Entities, die explicit mittels DataContract markiert werden. Durch das explizite Markieren werden keine ungewollten/unnötigen Daten transportiert. Man kann so außerdem etwas Einfluss auf die Datenmenge nehmen, in dem Standardwerte nicht transportiert werden. Mittels der Eigenschaft EmitDefaultValue kann genau dieses Verhalten verändert werden. WCF ist an sich außerdem sehr schlau, was die Serialisierung und Transport von Daten betrifft. Dies trifft vor allem auf properitären Protokolle mit dem BinaryFormatter zu. Daher würde ich immer empfehlen auch mindestens ein NET-TCP-Binding Endpoint bereitzustellen, so dass .NET Applikation von den internen Mechanismen stark profitieren können.
GC (Garbage Collection)
Kann man am GC etwas optimieren? Nicht viel, es gibt aber eine Einstellung, die die Anwendung positiv beeinflussen können. Man kann sich in jeder Anwendung entscheiden, ob man Workstation GC oder Server GC benutzt. Der Unterschied von beiden GCs ist nur maginal, bei der Server GC findet das Aufräumen in einem eigenen Thread pro CPU statt. Es gibt wahrscheinlich noch weitere Unterschiede, aber das ist der, den ich mir auch merken kann. Man kann den Server GC leicht mittels des Konfigurationsblocks anlegen:
1: <configuration>
2: <runtime>
3: <gcServer enabled=“true"/>
4: </runtime>
5: </configuration>
Hier noch ein schöner Artikel zu dem Thema: http://blogs.msdn.com/maoni/archive/2004/09/25/234273.aspx
Memory consumption
Irgendwas wollte ich hier noch reinpacken, wenn es mir wieder einfällt, dann trage ich es nach.