- 1Inhaltsverzeichnis
- 2C# Vortrag: Best Practices für Performance
- 2.1Verkettung von Zeichenketten (strings)
- 2.2Externe Resourcen frühzeitig freigeben - IDisposable
- 2.3Boxing/Unboxing vermeiden - Equals erneut implementieren
- 2.4Exceptions vermeiden
- Ergebnis von variableA + variable B,
- Ergebnis von variableA + variable B + variableC,
- Ergebnis von variableA + variable B + variableC + variableD,
- Ergebnis von variableA + variable B + variableC + variableD + variableE.
Inhaltsverzeichnis
C# Vortrag: Best Practices für Performance
Die Performance beschreibt die Leistung bzw. die Effizienz mit der eine Aufgabe erfüllt wird. Im Falle von Performanceverbesserungen im C# Umfeld wird immer darauf abgezielt die Programmablaufzeiten oder den Speicherverbrauch eines Programms oder einer Methode bei gleichem Ergebnis zu minimieren. Die hier aufgeführten Szenarien sind nur ein kleiner Teil dessen was alles möglich ist um die Programmlaufzeiten und Speichernutzung zu verbessern.
Verkettung von Zeichenketten (strings)
Zeichenketten sind im Speicher sind unveränderbar. Das bedeutet, dass bei jeder Änderung eines Strings eine neue Zeichenkette im Speicher abgelegt wird. Da hierbei jedes mal neuer Speicher allokiert werden muss, ist diese Operation relativ aufwändig. Wird nun eine Verkettung von variablen Zeichenketten angestrebt und mit dem += Operator verkettet passiert folgendes:
Wir gehen von dem unten gegebenen Code aus.
string variableA = "String 1,";
string variableB = "String 2,";
string variableC = "String 3,";
string variableD = "String 4,";
string variableE = "String 5";
string ergebnis = variableA + variableB + variableC + variableD + variableE;
Beim Erstellen des Ergebnises haben wir bereits 5 Zeichenketten im Speicher. Wird nun eine Verkettung gemacht haben wir neben den 5 Zeichenketten folgendes im Speicher stehen:
Wir haben also durch die Verkettung nicht einfach das Ergebnis, sondern auch jedes nicht benötigte Zwischenergebnis gespeichert. Dadurch wurde jedes mal unnötig neuer Speicher allokiert.
Um das Problem zu lösen gibt es in .NET unter dem Namespace System.Text die Klasse StringBuilder. Diese Klasse wurde speziell dafür geschrieben um variable Zeichenketten schnell zu verketten. Gerade bei Verkettung vieler variabler Zeichenketten oder bei Schleifenoperationen ist der StringBuilder sehr schnell, wie ein Performancetest zeigt. bei 50000 Verkettungen und 10 Iterationen braucht eine reguläre String-Verkettung mittel += rund 40 Sekunden. Der StringBuilder ist nach ~80ms fertig und zudem deutlich speicherschonender.
Stringverkettung - Testcode
public class StringConcat
{
private const int innerIterations = 50000;
private const int outerIterations = 10;
public void Concat()
{
for (int h = 1; h <= outerIterations; h++)
{
string concatResult = string.Empty;
for (int i = 0; i < innerIterations; i++)
{
concatResult += i.ToString();
}
}
}
public void ConcatStringBuilder()
{
for (int h = 1; h <= outerIterations; h++)
{
StringBuilder concatResult = new StringBuilder();
for (int i = 0; i < innerIterations; i++)
{
concatResult.Append(i);
}
concatResult.ToString();
}
}
}
Merke:- Verkettung variabler Zeichenketten müssen immer mit StringBuilder erfolgen um Speicher zu schonen und einen schnellen Programmablauf zu gewährleisten.
- Wenn nur eine Hand voll variabler Zeichenketten verknüpft werden kann man auch die Methode string.Format verwenden: https://msdn.microsoft.com/de-de/library/system.string.format%28v=vs.110%29.aspx
Tipp
Solange alle Zeichenketten, die verkettet werden sollen, eine konstante Länge aufweisen, können diese auch mit += verkettet werden. In dem Falle fasst .NET beim Kompilieren die Zeichenketten zu einer zusammen.
Externe Resourcen frühzeitig freigeben - IDisposable
In Anwendungen werden oftmals externe Resourcen genutzt. Dies kann zum Beispiel eine Datenbank sein, ein Filesystem, COM-Objekte oder auch ein Webservice. Bei Verwendung von externen Resourcen kann jedoch nie sichergestellt werden, dass diese fehlerfrei funktioniert oder überhaupt da ist. C# ist zwar eine Programmiersprache mit Managed Code und Garbage Collector, trotzdem führt schlechter Code auch hier zu Speicherlecks. Der Programmierer muss sich deshalb selbst um die Verwaltung der externen Resource kümmern, auf Fehler reagieren und die Resource zum Schluß wieder ordnungsgemäß freigeben. Tut er dies nicht ist die Folge das Blockieren einer Resource. Das kann weitreichende Auswirkungen haben. Unter Betrachtung der o.g. Beispiele wären das unter anderem:
- nicht geschlossene Datenbankverbindung blockiert andere Anwendungen/Anwender der Datenbank
- geöffnete Datei lässt sich nicht von weiteren Programmen öffnen/überschreiben
- Objekte im Speicher verweilen dort auch nach Programmende. Erst ein Neustart des Betriebssystems sorgt dafür das diese verschwinden.
- eine Verbindung zum Webservice bleibt bestehen und blockiert andere Anwendungen/Anwender des Webservices
- etc.
In .NET gibt es zum Freigeben von Resourcen die Schnittstelle IDisposable. Wenn eine Klasse diese Schnittstelle implementiert, besitzt die Klasse die Methode Dispose(). Weiterhin kann eine Klasse, die diese Schnittstelle implementiert, über einen using-Block gesteuert werden. Dies sorgt dafür, dass am Ende eines using-Blocks die Methode Dispose() aufgerufen wird. Man könnte natürlich auch am Ende der Arbeit die Methode Dispose von Hand aufrufen. Dabei kann es bei schlechter Programmierung aber zum genannten Problem kommen. Folgender Code veranschaulicht dies.
Resource ohne using
try
{
FileStream fileStream = new FileStream("C:\\Vortrag\\Outputfile.txt", FileMode.Create);
StreamWriter streamWriter = new StreamWriter(fileStream);
streamWriter.WriteLine("Hier stehen ganz viele Daten.");
throw new InvalidOperationException("Fehler");
streamWriter.Close();
streamWriter.Dispose();
fileStream.Close();
fileStream.Dispose();
}
catch (Exception)
{
//Hier wird die Ausnahme behandelt.
}
finally
{
//Hier wird finaler Code ausgeführt
}
Wir öffnen eine Textdatei um etwas darin zu ergänzen. Es tritt dabei ein unerwarteter Fehler auf. Durch diese Ausnahme wird in den catch-Block gesprungen, ein Close() oder Dispose() auf die Objekte findet nicht statt. Die Datei kann somit nicht mehr verändert werden, da auf ihr noch eine Sperre liegt. Durch die Verwendung von using kann dies verhindert werden:
Resourcen mit using
try
{
using (FileStream fileStream = new FileStream("C:\\Vortrag\\Outputfile.txt", FileMode.Create))
{
using (StreamWriter streamWriter = new StreamWriter(fileStream))
{
streamWriter.WriteLine("Hier stehen ganz viele Daten.");
throw new InvalidOperationException("Fehler");
}
}
}
catch (Exception)
{
//Hier wird die Ausnahme behandelt.
}
finally
{
//Hier wird finaler Code ausgeführt
}
Tritt nun innerhalb eines eines using-Blocks eine Ausnahme auf, wird für beide Blöcke die Methode Dispose() aufgerufen, die die Resourcen wieder freigibt. Manchmal ist es auf Grund der Programmstruktur nicht immer möglich ein using zu verwenden. In dem Falle müssen die benötigten Klassen vor dem try-Block instanziiert und im finally-Block freigegeben werden.
Dispose mit try-finally
FileStream fileStream = new FileStream("C:\\Vortrag\\Outputfile.txt", FileMode.Create);
StreamWriter streamWriter = new StreamWriter(fileStream);
try
{
streamWriter.WriteLine("Hier stehen ganz viele Daten.");
throw new InvalidOperationException("Fehler");
}
catch (Exception)
{
//Hier wird die Ausnahme behandelt.
}
finally
{
streamWriter.Close();
streamWriter.Dispose();
fileStream.Close();
fileStream.Dispose();
}
Merke:
- Wann immer möglich sollten externe Resourcen, die IDisposable implementieren, mit "using" behandelt werden.
- Bei Verwendung externer Klassen oder denen des .NET Frameworks muss immer geprüft werden ob sie die Schnittstelle IDisposable implementiert.
- Beim Verwenden der Schnittstelle in eigenen Klassen muss man sich in der Dispose-Methode selber um das Freigeben von Resourcen und Schließen von Verbindungen kümmern.
Tipp
Es empfiehlt sich externe Resourcen so spät wie möglich zu öffnen/verwenden und so früh wie möglich wieder freizugeben.Gerade bei Anwendungen mit vielen Benutzern kann dies einen großen Performanceunterschied machen.
Boxing/Unboxing vermeiden - Equals erneut implementieren
Unter Boxing versteht man die Umwandlung eines Wertetyps in den Typ System.Object. Unboxing ist der umgekehrte weg von einem Object in einen anderen Typ. Beide Operationen sind sehr CPU und zeitlastig und sollten deshalb nur verwendet werden, wenn es unbedingt benötigt wird. Zudem wird für diese Operation mehr Speicher benötigt, da ein zusätzlicher Zeiger (Pointer) zu der Referenz benötigt wird und der Wert zusätzlich auf dem Heap abgelegt wird.
Ältere .NET Klassen wie z.B. ArrayList und HashTable arbeiten intern mit der Klasse System.Object. Beim Aufruf der Add-Methode von einer der beiden Klassen wird jedes mal eine Boxing-Operation durchgeführt. Zur Verbesserung der Performance sollten diese Klassen deshalb besser vermieden werden. Stattdessen ist es vorzuziehen generische Klassen mit Typsicherheit zu verwenden wie z.B. List<T> oder Dictionary.
Boxing / Unboxing
int myValue = 12;
//Boxing
object obj = myValue;
if (obj is int)
{
//Unboxing
int getMyValue = (int)obj;
}
Wann immer man mit der Klasse System.Object in Methoden arbeitet kann man davon ausgehen, dass hier eine Boxing/Unboxing Operation stattfindet. Manchmal hat man als Programmierer jedoch die Möglichkeit dieses Problem zu umgehen. Wenn man beispielsweise eine Methode überlädt, so kann man hier eine eigene Implementierung mit einem typisiertem Objekt schreiben und diese Operation somit umgehen um noch etwas Performance zu gewinnen. Ein Beispiel hierfür wäre z.B. die Überladung der oft verwendeten Equals-Methode, die jedes Objekt besitzt und zwei Objekte vergleicht. Bei einer Überladung würde das .NET Framework dann auf die typsichere Methode zurückgreifen, wodruch man unnötiges Umwandeln (Unboxing) und Abfragen des Ergebnisses spart. Dies kann gerade bei vielen Aufrufen der Methode ein entscheidender Performancefaktor sein. Die Equals-Methode ist dabei nur ein Beispiel für eine mögliche Optimierung. Folgender Codeblock veranschaulicht die beschriebene Verbesserung:
Equals verbessern
// normale Equals Implementierung mit Unboxing
public override bool Equals(object obj)
{
if (obj == null)
{
return false;
}
// Hier muss eine Umwandlung stattfinden und diese auch geprüft werden
Person person = obj as Person;
if (person == null)
{
return false;
}
return person.PersonID == this.PersonID;
}
// verbesserte Equals Implementierung ohne Unboxing
public bool Equals(Person obj)
{
if (obj == null)
{
return false;
}
// Umwandlung (Unboxing) und Prüfung nach Umwandlung können hier entfallen
return obj.PersonID == this.PersonID;
}
Merke:
- Vermeide die Klassen ArrayList und HashTable -> Stattdessen List<T> und Dictionary mit sicheren Typen verwenden (nicht List<object>, sondern List<Klasse>)
- Vermeide Boxing/Unboxing wenn möglich um Rechenzeit und Arbeitsspeicher zu sparen
- Versuche Methoden mit Parameter System.Object zu verbessern, wenn diese oft verwendet werden
Exceptions vermeiden
Die Exception-Klasse beinhaltet sehr viele Informationen über aufgerufene Methoden, intern ausgelöste Ausnahmen, verwendete Typen etc. Diese Informationen werden über Traces und Reflection mitgeloggt. Der Aufbau dieses Objektes und das Abrufen dieser Informationen ist relativ aufwändig. Wird eine Methode häufig aufgerufen oder die Anwendung von vielen Benutzern gleichzeitig verwendet, kann es hier schnell zu Performanceeinbußen kommen, da der gesamte StackTrace für jeden Aufruf/Benutzer mitgeloggt werden muss. Es empfiehlt sich daher möglichst auf Exceptions zu verzichten. Das soll keinesfalls heißen, dass man gänzlich darauf verzichtet, sondern eher, dass man sie als das betrachtet was der Name schon impliziert: eine Ausnahmesituation. Bei vielen Operationen ist durch vorherige Prüfung oder bessere Programmierung das Abfangen einer Ausnahme unnötig. Die beiden folgende Beispiele verdeutlichen dies. Es gilt auch iher, dass es sich nur um Beispiele handelt und es noch mehr Möglichkeiten Exceptions zu vermeiden.
Ausnahmen vermeiden - NullReferenceException
// Es wird davon ausgegangen, dass eine Suche oder Operation stattgefunden hat, bei der eine Person gesucht wurde
Person person = null;
try
{
// Zugriff auf Objekt null löst die Ausnahme aus
int id = person.PersonID;
}
catch (NullReferenceException)
{
//Ausnahme behandeln
}
// besser -> vor dem Zugriff das Objekt auf null prüfen!
Person person = null;
if(person != null)
{
int id = person.PersonID;
}
else
{
// Fehler behandeln
}
Ausnahmen vermeiden - InvalidCastException (Konvertierung von Datentypen)
// Unwandeln direkt über int.Parse und fangen der Ausnahme ist nicht effektiv
try
{
string wert = "abc";
int intWert = int.Parse(wert);
}
catch(InvalidCastException)
{
// Ausnahme behandeln
}
// besser -> TryParse Methoden verwenden
string wert = "abc";
int intWert;
if (!int.TryParse(wert, out intWert))
{
//wenn es nicht geklappt hat
}
Merke:
- Ausnahmen wann immer es möglich ist vermeiden und im Vorfeld zu überlegen ob sie durch vernünftige Prüfungen ersetzt werden kann
- Ausnahmen sollten auch Ausnahmen bleiben.
Richtig konvertieren
In Anwendungen ist es oftmals nötig Datentypen in andere zu überführen. Dabei stellt sich auch oft die Frage was eigentlich am besten/schnellsten ist um einen Datentyp zu ändern. Hier ist deshalb einmal eine kurze Auflistung der Möglichkeiten am Beispiel eines Integers.
Umwandeln von Datentypen
object integerWert = 500;
string stringAlsInteger = "500";
// Wenn man weiss, dass es ein geboxter Integer-Wert ist kann man hart casten..
int wertUnboxed = (int)integerWert;
// ...oder auch weich wenn es sich auf jeden Fall um ein Unboxing handelt
int? wertUnboxedSoft = integerWert as int;
if(!wertUnboxedSoft.HasValue)
{
// behandeln, wenn Cast fehlgeschlagen
}
// Wenn man weiss, dass es sich um einen String handelt und 100% sicher ist, dass dort ein Integer enthalten ist
int wertParsed = int.Parse(stringAlsInteger);
// Wenn man weiss, dass es sich um einen String handelt, aber nicht weiss ob es sich um einen Integer handelt
int wertMaybeParsed;
if (!int.TryParse(stringAlsInteger, out wertMaybeParsed))
{
//behandeln, wenn nicht geparsed werden konnte, ansonsten steht der geparste Wert in "wertMaybeParsed"
}
// Wenn man nicht weiss ob der Integer boxed vorliegt oder als String
// Achtung: Hier kann ggf. InvalidCastException auftreten, die behandelt werden muss
int wertVonIrgendetwas = Convert.ToInt32(integerWert);
Merke:
- Überlege genau wann was die beste Methode ist um Werte umzuwandeln
- Wenn möglich sollte auf Convert.To-Methoden verzichtet werden, da sie immer eine Unboxing-Operation durchführen und dessen Ausnahmen gerade in Schleifen ein Performancekiller darstellen.


No comments:
Post a Comment