
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::MainIn 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::MainIm 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.
Wenn ihnen der Artikel gefallen hat oder er für sie hilfreich war, bitten "kicken" sie ihn.
