 RSS-Feed
|
|
 RSS-Feed - Einträge
|
|
|
|
|
|
|
|
In einem aktuellen Projekt machte mich NDepend auf einen übermäßigen Gebrauch von Boxing und Unboxing aufmerksam. Hauptsächlich wurde dabei eine Klasse angemeckert, welche die Einstellungen aus der web.config für eine Anwendung verfügbar macht, die keinen Zugriff auf die hostende Webanwendung hat. In besagter Klasse wurden diverse Einstellungswerte verschiedenen Typs in einer Object Collection gehalten und beim Abruf aus dieser in den entsprechenden Wertetyp gecastet. Also jede Menge Boxing- und Unboxing-Vorgänge. Bei der Recherche zu dem Thema, warum man Boxing und Unboxing denn vermeiden solle, stieß ich immer wieder auf die Aussage: Boxing/Unboxing ist sehr rechenintensiv und kann sich daher negativ auf die Gesamtleistung einer Anwendung auswirken. In der MSDN heißt es unter anderem im Artikel Leistung (C# und Visual Basic) im Absatz über Boxing und Unboxing: Wenn ein Wertetyp mittels Boxing konvertiert wird, muss ein völlig neues Objekt erstellt werden.Dies kann bis zu 20-mal länger dauern als eine einfache Zuweisung eines Verweises. Das war es Wert näher untersucht zu werden. Für den Anfang beschränkte ich meinen Versuch auf die Primitiven. Der Einfachheit halber verwende ich hier Int32-Werte, welche in ein Array vom Typ Object geschrieben werden. Anschließend werden die Werte aus diesem Object-Array mit verschiedenen Methoden in ein Array vom Typ Int32 geschrieben und die Ergebnisse verglichen. Das Object-Array wird immer mit 1 Millionen Werten von 0 aufsteigend gefüllt. Als erstes habe ich ein einfaches Boxing verwendet um das Object-Array mit Int32-Werten zu füllen. Anschließend werden via Unboxing die Werte aus dem Object-Array in das Int32-Array übertragen. namespace BoxingUnboxingTest
{
using System;
using System.Diagnostics;
using System.Globalization;
class Program
{
private static object[] container;
private static int[] result;
private const int MaxItems = 1000000;
static void Main(string[] args)
{
container = new object[MaxItems];
result = new int[MaxItems];
var sw = new Stopwatch();
sw.Start();
FillArrayWithInt();
sw.Stop();
var timeToFill = sw.ElapsedMilliseconds;
sw.Start();
FillResult(false, false);
sw.Stop();
var timeToTransfer = sw.ElapsedMilliseconds - timeToFill;
var totalTime = sw.ElapsedMilliseconds;
}
private static void FillArrayWithInt()
{
for (int i = 0; i < MaxItems; i++)
{
container[i] = i;
}
}
private static void FillResult(bool useParse, bool useConvert)
{
if (useParse)
{
for (int i = 0; i < MaxItems; i++)
{
result[i] = int.Parse(container[i].ToString(), CultureInfo.InvariantCulture);
}
return;
}
if (useConvert)
{
for (int i = 0; i < MaxItems; i++)
{
result[i] = Convert.ToInt32(container[i], CultureInfo.InvariantCulture);
}
return;
}
for (int i = 0; i < MaxItems; i++)
{
result[i] = (int)container[i];
}
}
}
}
Das Ergebnis war erst einmal verblüffend. Ich erwartete, auf Grund der vielen boxing/unboxing Operationen ein katastrophales Ergebnis. Statt dessen wurde mir ein sehr flottes Resultat angezeigt: Für das Füllen des Object-Array, das Boxing, wurden im Schnitt 50 Millisekunden benötigt. Für das Umkopieren der Werte in das Int32-Array mittels Unboxing lediglich 15 Millisekunden im Mittel.
Da ich ja vom Boxing/Unboxing weg sollte, probierte ich als nächstes eine Version mit der Convert-Klasse in der ich das boxing in das Object-Array beibehielt, das unboxing mit einer Überladung der ToInt32-Methode ersetzte. Das Ergebnis war ernüchternd. Anstatt schneller zu werden, ich hatte ja auf das böse unboxing verzichtet, benötigte diese Variante satte 1250 Millisekunden im Mittel.
Die Verwendung der Methode int.Parse war, mit 1850 Millisekunden im Mittel, die langsamste Variante.
Als nächstes startete ich den gleichen Versuch mit Werten des Typs DateTime. Auch hier begann ich mit dem Boxing/Unboxing und füllte das Object-Array wieder mit 1 Millionen Werten. Das Füllen des Array dauerte diesmal rund 1500 Millisekunden im Mittel. Das Umkopieren via Unboxing benötigte etwa 15 Millisekunden. Das Unboxing von DateTime-Werten geht also genauso schnell wie das Unboxing von einfachen Int32-Werten.
Als nächstes testete ich das Verhalten mit der Convert-Klasse in dem ich die ToDateTime Methode verwendete und dieser die Werte als Typ Object übergab. Auch hier wieder das gleiche Ergebnis wie bei den Int32-Werten: etwa 1250 Millisekunden im Mittel.
Auch bei DateTime-Objekten benötigte die Parse-Methode DateTime.Parse mit rund 4000 Millisekunden am längsten.
Erstes Zwischenfazit:
Für den reinen Transport von öfter wechselnden Werten verschiedenen Typs in einem Container mit einer Auflistung vom Typ Object, ist die Verwendung von Boxing/Unboxing die erste Wahl.
Ich konnte keine Methode finden, welche die Aufgabe schneller und einfacher erledigt.
Aber damit ist das Thema noch nicht ausgereizt. Boxing/Unboxing wird auch innerhalb des Framework in den verschiedensten Methoden, für den Benutzer oft vollkommen transparent, verwendet. Ein typischer Vertreter dieser Art ist die oft verwendete Methode String.Format mit ihren diversen Überladungen.
In folgendem Beispiel werden die beiden Integer Werte in ein Object geboxt und anschließend die Methode Object.ToString aufgerufen.
var output = string.Format(
CultureInfo.InvariantCulture,
"Dies ist Wert Nr.{0} von {1} Werten insgesamt.",
5,
100);
Um hier das Boxing zu vermeiden könnte anstatt der Integer Werte, die jeweilige Darstellung als Zeichenfolge an die Methode übergeben werden:
var output = string.Format(
CultureInfo.InvariantCulture,
"Dies ist Wert Nr.{0} von {1} Werten insgesamt.",
5.ToString(CultureInfo.InvariantCulture),
100.ToString(CultureInfo.InvariantCulture));
Da die Methode int.ToString ein Member der int Struktur ist, wird hier kein Boxing benötigt. Ob dieses Vorgehen einen Vorteil gegenüber der direkten Angabe der Integer Werte darstellt, gilt es zu ermitteln.
Als erstes habe ich ein Array vom Typ string mit 1 Millionen Werten mit der Methode string.Format gefüllt, in dem ich den jeweiligen Wert zwischen 0 und 999999 als Integer Wert an die Methode übergeben habe. Dieses Verfahren benötigte im Schnitt 3700 Millisekunden.
Die Angabe der Zeichenfolge mit Hilfe der int.ToString Methode gegenüber der direkten Angabe der Integer Werte brachte auch dieses mal keine Verbesserung der Leistung. Diese Variante benötigte zur Erstellung der 1 Millionen Werte etwa 5000 Millisekunden im Mittel.
Zweites Zwischenfazit:
Für Zahlenwerte jeglicher Art scheint die Verwendung von Boxing/Unboxing die schnellste und effizienteste Art zu sein, die jeweiligen Werte zwischen verschiedenen Objekten zu übertragen oder als formatierte Zeichenfolge darzustellen.
Als letzten Versuch möchte ich mich einer anderen Art der Wertetypen zuwenden, dem enum. Enumerationen sind oft ein heißes Diskussionsthema: Die einen lieben und die anderen hassen sie. Manche bezeichnen sie gar als böse. Diese Diskussion soll hier außen vor bleiben. Ich betrachte sie für diesen Versuch als das was sie sind: Ein weiterer Wertetyp.
Für diesen Versuch verwende ich wieder die Methode string.Format um eine formatierte Zeichenfolge darzustellen, in der der Wert einer Enumeration verwendet wird.
Bei der Zeichenfolgendarstellung einer Enumeration mit den Mitteln des Framework, wird immer ein Boxing durchgeführt. Egal ob in der Methode string.Format oder mit der ToString Methode einer Enumeration. Ein Blick in den IL-Code des folgenden Beispiel zeigt dieses Verhalten sehr schön. Zuerst der Code in C#:
static void Main(string[] args)
{
var output = TestEnum.Entry8.ToString();
}
Und anschließend der erzeugte IL-Code:
.method private hidebysig static
void Main (
string[] args
) cil managed
{
// Method begins at RVA 0x2050
// Code size 14 (0xe)
.maxstack 1
.entrypoint
.locals init (
[0] string output
)
IL_0000: nop
IL_0001: ldc.i4.8
IL_0002: box BoxingUnboxingTest.TestEnum
IL_0007: callvirt instance string [mscorlib]System.Object::ToString()
IL_000c: stloc.0
IL_000d: ret
} // end of method Program::Main
In Zeile 15 und 16 ist gut zu sehen, wie zuerst der Wert der Enumeration in ein Object geboxt und anschließend die Methode Object.ToString aufgerufen wird.
Zum füllen eine Array vom Typ String mit 1 Millionen Werte benötigte die Methode string.Format mit dem Boxing im Schnitt etwa 13800 Millisekunden.
Im das Boxing zu vermeiden, habe ich mich für eine Erweiterungsmethode entschieden, welche in einem switch/case-Block die Zeichenfolgendarstellung des angegebenen Wertes der Enumeration zurückgibt:
internal static string GetName(this TestEnum value)
{
switch (value)
{
case TestEnum.Entry1:
return "Entry1";
case TestEnum.Entry2:
return "Entry2";
case TestEnum.Entry3:
return "Entry3";
case TestEnum.Entry4:
return "Entry4";
case TestEnum.Entry5:
return "Entry5";
case TestEnum.Entry6:
return "Entry6";
case TestEnum.Entry7:
return "Entry7";
case TestEnum.Entry8:
return "Entry8";
case TestEnum.Entry9:
return "Entry9";
default:
return "None";
}
}
Wenn nun das vorangegangene Beispiel mit dieser Erweiterungsmethode wiederholt wird, sollte das Boxing aus dem IL-Code verschwunden sein.
.method private hidebysig static
void Main (
string[] args
) cil managed
{
// Method begins at RVA 0x2050
// Code size 9 (0x9)
.maxstack 1
.entrypoint
.locals init (
[0] string output
)
IL_0000: nop
IL_0001: ldc.i4.8
IL_0002: call string BoxingUnboxingTest.Extensions::GetName(valuetype BoxingUnboxingTest.TestEnum)
IL_0007: stloc.0
IL_0008: ret
} // end of method Program::Main
Im Gegensatz zum vorherigen IL-Code Beispiel wird in Zeile 15 kein Boxing mehr durchgeführt, sondern statt dessen die Erweiterungsmethode aufgerufen die ja den erwarteten String zurückgibt.
Ist die Vermeidung des Boxings dieses mal messbar?
Ja, ist es. Die Variante unter Verwendung der Erweiterungsmethode benötigte rund 9270 Millisekunden. Sie ist rund ein Drittel schneller als die Vorherige Variante in der Boxing verwendet wird.
Fazit:
In den meisten Fällen, zumindest wenn Primitiven oder Zahlenwerte verwendet werden, braucht auf das Boxing und Unboxing keine besondere Rücksicht genommen werden.
Doch bereits bei der Verwendung von Werten aus Enumerationen ändert sich das Bild. Hier sollte im Einzelfall geprüft werden, wie sich der erzielte Nutzen im Verhältnis zum Aufwand verhält. Bei deutlich komplexeren Strukturen sind Tools wie etwa NDepend sehr hilfreich um festzustellen, ob und wo eventuell Boxing oder Unboxing verwendet wird,
Für mich ist das Boxing und Unboxing nun nicht mehr so böse, wie es oft dargestellt wird.
Natürlich können in verschiedenen Situationen Leitungseinbußen durch das Boxing oder Unboxing entstehen. Aber deshalb dieses überaus hilfreiche Compiler Feature per se als böse zu bezeichnen, schießt doch etwas über das Ziel hinaus.
Technorati-Tags: CLR | C# | Analysis | Sourcecode | boxing | unboxing
|
|
04.06.2011 - 14:38:25
|
|
http://feedproxy.google.com/~r/klaus_b/~3/HlENwAZql74/post.aspx
|
|
|
|
|
|
|
|
|
Kurz vor Ostern erhielt ich von Patrick Smacchia, dem verantwortlichen Entwickler hinter NDepend, eine Anfrage, ob ich Interesse hätte mir NDepend einmal anzusehen. Die eingeholten Vorabinformationen aus der deutschen .NET-Gemeinde waren durchweg positiv und erzeugten eine gewisse Neugier. Warum also nicht eine eigene Meinung bilden? Ich bekundete mein Interesse beim NDepend-Team und erhielt kurz darauf eine Lizenz. Der Download fällt mit 9,4 MB eher klein aus. Eine echte Installation wird nicht benötigt. Es müssen lediglich die Daten aus dem ZIP-Archiv in ein privates Verzeichnis entpackt werden. Das entpacken der Daten in den Programmeordner sollte wegen möglicher Probleme mit der Benutzerkontensteuerung (UAC) unterbleiben. Nach dem Entpacken muss, für die Integration von NDepend in Visual Studio, einmal die Anwendung NDepend.Install.VisualStudioAddin.exe ausgeführt werden. In einem Auswahldialog wird nach der verwendeten Visual Studio Version gefragt. Es kann zwischen voller Integration und einer, Light Integration genannten, teilweisen Integration gewählt werden. Die von NDepend verwendeten Shortcuts können auf Wunsch deaktiviert werden, falls Kollisionen mit bereits verwendeten Tastenkürzeln zu befürchten sind. Ab jetzt steht NDepend zur Codeanalyse in Visual Studio zur Verfügung. Um NDepend mit einem Visual Studio Projekt zu verwenden, kann entweder eine neues NDepend-Projekt erzeugt und mit dem aktuellen Visual Studio-Projekt verknüpft, oder das aktuelle Visual Studio-Projekt mit einem bestehenden NDepend-Projekt verknüpft werden. Auf der Seite der Einstellungen des NDepend-Projekts, ist der wichtigste Punkt die richtige Auswahl der zu verwendenden Framework-Version. Mit dieser Einstellung wir festgelegt, aus welchen Ordnern NDepend die referenzierten Assemblies des Framework lädt. Die weiteren Einstellungen, etwa zur Analyse und zum Aussehen des erzeugten Reports, sind selbsterklärend. Nach der ersten Analyse des aktuellen Projekts, steht in der linken unteren Ecke in Visual Studio ein kleiner farbiger Kreis zur Verfügung, der den aktuellen Status der Analyse des Projekts anzeigt. Es werden die Ampelfarben Rot, Gelb und Grün, sowie Grau und Blau mit folgender Bedeutung verwendet: Rot - Eine oder mehrere der als kritisch eingestuften Regeln wurde verletzt - Eine oder mehrere der aktivierten Regeln kompiliert nicht - Eine oder mehrere der aktuellen Abfragen ist fehlerhaft. Gelb Eine oder mehrere der aktivierten Regeln wurde verletzt. Grün Es wurde keine der aktivierten Regeln verletzt. Grau besagt, dass kein NDepend-Projekt zur Verfügung steht. Blau wird angezeigt, während eine Analyse ausgeführt wird. Beim Überfahren des Symbols mit der Maus wird ein Menü sichtbar. In diesem Menü können, unter anderem, die Einstellungen zur Ausführung der Analyse sowie zur Aktualisierung der Analyse in Visual Studio fein eingestellt werden. Es kann auch eine Analyse des aktuellen Projekts direkt angestoßen werden. Im oberen Bereich wird der aktuelle Status des Projekts in einer Zusammenfassung gezeigt. Den Kern der Analyse bildet die in NDepend integrierte Code Query Language (CQL). Dies Abfragesprache erinnert stark an SQL und ist auch sehr ähnlich zu verwenden. Zum Bearbeiten bestehender Abfragen oder um neue Regeln zum aktuellen Projekt hinzuzufügen, steht ein integrierter Editor zur Verfügung. Mit diesem Konzept und dem integrierten Editor ist es sehr einfach, bestehende Regeln den aktuellen Anforderungen anzupassen oder neue Regeln zu erstellen. Eine der Regeln besagt: Felder welche auf die Bezeichnung Uri enden, sollten vom Typ System.Uri sein. Es kann aber wie in folgendem Beispiel vorkommen, dass der Namensteil Uri verwendet wird aber dennoch der Typ Uri nicht verwendet werden kann. Zum Beispiel ein Feld vom Typ ConfigurationProperty mit dem Namen serviceUri würde diese Regel verletzen. Mit dem integrierten CQL-Editor wird einfach als optionales Kriterium nicht vom Typ ConfigurationProperty zur Abfrage hinzugefügt. Die jeweiligen Änderungen oder Anpassungen des Regelwerks sind allerdings nur für das aktuelle Projekt gültig. Ich habe noch keinen Weg gefunden, um allgemeingültige Änderungen des Regelwerks vorzunehmen. NDepend analysiert nicht den Quellcode, sondern den erzeugten IL-Code in den kompilierten Assemblies. Um die jeweiligen Stellen im Quellcode zeigen zu können, werden Debug-Assemblies mit der zugehörigen PDB-Datei benötigt. Eine Analyse von Assemblies mit Release-Status ist nicht vorgesehen. Soweit zur Arbeits- und Funktionsweiße. Als erstes Projekt zur Analyse mit NDepend habe ich mir eine kleine Anwendung ausgesucht, die als Modul in ASP.NET Anwendungen zum Einsatz kommt. Die Anwendung hat kein UI, wird ausschließlich über die web.config konfiguriert und von einem Http-Modul gestartet. Der erste Report brachte eine Fülle von Informationen und jede Menge Warnungen zu Tage. Die für mich interessanteste Warnung betraf die Sichtbarkeit des verwendeten Http-Moduls, der enthaltenen Http-Handler und der Klasse der Einstellungen die von ConfigurationSection  abgeleitet ist. NDepend empfahl diese Klassen als internal zu deklarieren. Und wie soll dann bitte die Webanwendung auf diese Typen zugreifen? Egal; das wollte ich wissen. Ich deklarierte also die betreffenden Klassen als internal, kompilierte das Projekt im Release-Status neu und startete eine Webanwendung in der ich das Modul verwendete. Zu meiner großen Überraschung funktionierte alles wie gewohnt. Mit dem positiven Nebeneffekt, dass ich keine öffentliche API mehr zur Verfügung stellte die auch so nie vorgesehen war. OK. Wieder etwas dazugelernt. NDepend ist auch das erste, mir bekannte, Analyse-Tool welches auf die Unveränderbarkeit von Strukturen (immutable struct) achtet. Hier wird geprüft, ob auch wirklich alle Felder der Struktur als readonly deklariert sind. Dadurch wurde ich angehalten, mich mit der Implementierung von entsprechenden Replace-Methoden in solche unveränderliche Strukturen zu befassen. Aber das ist ein anderes Thema. Im Allgemeinen sind die Standard-Regeln bereits sehr restriktiv ausgelegt. Wenn sie befolgt und nicht umgangen werden, resultiert aus der konsequenten Anwendung ein deutlich wartbarerer Code. NDepend kann natürlich noch deutlich mehr als ich hier in ein paar wenigen Zeilen schreiben kann. Ich werde nach und nach, wenn ich NDepend etwas besser kenne, auf die einzelnen Feature eingehen. Fazit: NDepend erscheint mir als ein hilfreiches Werkzeug, sehr gut geeignet zur täglichen Verwendung. Die Entwickler hinter NDepend leisten sehr gute Unterstützung bei eventuellen Problemen und helfen schnell und unkompliziert. Bei meinen Recherchen vor der Verwendung von NDepend, sagte mit einer der Gefragten eine steile Lernkurve mit NDepend voraus. Dem kann ich mich nur anschließen. Technorati-Tags: Review | NDepend | Visual Studio | Codeanalyse
|
|
01.05.2011 - 18:03:57
|
|
http://feedproxy.google.com/~r/klaus_b/~3/sVICZKZ0yNo/post.aspx
|
|
|
|
|
|
|
|
|
|
 Unser RSS-Feed
Natürlich haben wir auch einen eigenen RSS-Feed den Du gerne abonnieren kannst ...
|
|
 Suche im Verzeichnis
RSS Verzeichnis nach Blog Feeds oder Tags durchsuchen:
|
|
 Login
|
|
|
|
|
|
 Kategorien
|
|
 Top Tags im Verzeichnis
|
|
 Tipps zum Surfen
|
|
|