Da neuere Computer kaum noch mit Prozessoren mit nur einem Kern bestückt werden, wird sich früher oder später ein jeder einmal Gedanken über die Parallelisierung seiner Anwendungen machen müssen. Die Parallel Extension to the .NET Framework 3.5, derzeit als zweite CTP vorliegend, bietet einen relativ einfachen Einstieg in die Parallelisierung von rechenintensiven Aufgaben. Interessierte können die Juni CTP hier herunter laden.
Wie überall gibt aus beim parallelisieren Aufgaben die gut und solche die weniger gut geeignet sind. Methoden die sehr E/A-intensive Aufgaben erledigen sollen, eignen sich eher nicht so gut zur Parallelisierung, da jeder Thread so lange blockiert wird, bis der Zugriff auf die jeweilige Ressource wieder frei gegeben ist. Gerade aus diesem Grund habe ich ein Beispiel gewählt das genau solch eine Aufgabe erledigen soll, um die Leistungsfähigkeit der TPL (Task Parallel Library) zu zeigen. In der gestellten Aufgabe gilt es alle vorhandenen Assemblies im GAC aufzulisten und die zugehörigen Installationsreferenzen zu ermitteln. Sowohl das Auflisten der Assemblies als auch das Ermitteln der Referenzen erfolgt mit Hilfe der Fusion-API. Um überhaupt eine Größe zu haben mit der ich vergleichen konnte, habe ich die einzige mir bekannte Anwendung verwendet welche auch die Fusion-API nutzt; die gacutil.exe aus dem SDK. Ich hoffe hier nicht gegen Punkt 2 der Eula, welcher Vergleichsstests regelt, des .NET Framework zu verstoßen. Zur Ermittlung der Referenz-Zeit habe ich gacutil.exe mit dem Argument -lr in einem Prozess gestartet und mit der Stopwatch-Klasse die benötigte Zeit ermittelt. hier der Code:
Process proc = new Process();
proc.StartInfo.FileName = @"C:\Programme\Microsoft SDKs\Windows\v6.0A\bin\gacutil.exe";
proc.StartInfo.Arguments = "-lr";
Stopwatch stopWatch = new Stopwatch();
stopWatch.Start();
proc.Start();
proc.WaitForExit();
stopWatch.Stop();
TimeSpan elapsed = TimeSpan.FromMilliseconds((double)stopWatch.ElapsedMilliseconds);
Diese Methode habe ich fünf mal ausgeführt. Dabei ergab sich, auf meiner Maschine, ein Mittelwert von 55,638 Sekunden für 676 Elemente. Somit war die Referenz-Zeit ermittelt als auch die Anzahl der Elemente die gefunden werden mussten.
Als Container zum aufnehmen der gefundenen Daten, habe ich mich für eine NameValueCollection entschieden. Um das, pro Iteration, gefundene Name-Wert Paar in die Kollektion einzufügen, habe ich zwei verschiedene Wege getestet. Als ersten das direkte Anfügen des Schlüssel-Wert Paares als Zeichenfolgen mit der NameValueCollection.Add(String, String)-Methode. Als zweiten Weg das Erzeugen einer zweiten NameValueCollection und diese mit der NameValueCollection.Add(NameValueCollection)-Methode in die Haupt-Kollektion einzufügen. Da die statischen for und foreach Methoden der TPL mit Delegaten arbeiten, habe ich zwei Hilfsmethoden erstellt um diese als Delegaten zu nutzen. Folgend die beiden Hilfsmethoden AddToCollection(String) und GetNameValuePair(String).
Die Methode AddToCollection(String) ist vom Typ void und fügt das Schlüssel-Wert Paar als Zeichenfolge an eine, als Class Member instanziierte, NameValueCollection an.
private static void AddToCollection(string assemblyName)
{
AssemblyCacheInstallReferenceEnum instRefEnum =
new AssemblyCacheInstallReferenceEnum(assemblyName);
InstallReference instRef = instRefEnum.NextReference;
// prüfen ob eine InstallReferenz-Instanz übergeben wurde
if (instRef != null)
{
StringBuilder sb = new StringBuilder(255);
sb.AppendFormat(CultureInfo.CurrentCulture,Resources.AsmRefSchemaMsg,InstallReferenceGuid.GetGuidSchemeName(instRef.GuidScheme));
sb.Append(" ");
sb.AppendFormat(CultureInfo.CurrentCulture,Resources.AsmRefIDMsg,instRef.Identifier);
sb.Append(" ");
sb.AppendFormat(CultureInfo.CurrentCulture,Resources.AsmRefDescriptionMsg,instRef.NonCanonicalData);
assemblyCollection.Add(assemblyName, sb.ToString());
}
else
{
// keine InstallReferenz gefunden, NULL anfügen
assemblyCollection.Add(assemblyName, null);
}
}Die Methode GetNameValuePair(String) ist vom Typ NameValueCollection. Diese zurückgegebene Kollektion wird an eine, in der aufrufenden Methode instanziierte, Kollektion angehängt.
private static NameValueCollection GetNameValuePair(string assemblyName)
{
NameValueCollection collection = new NameValueCollection(1);
AssemblyCacheInstallReferenceEnum instRefEnum = new AssemblyCacheInstallReferenceEnum(assemblyName);
InstallReference instRef = instRefEnum.NextReference;
// prüfen ob eine InstallReferenz-Instanz übergeben wurde
if (instRef != null)
{
StringBuilder sb = new StringBuilder(255);
sb.AppendFormat(CultureInfo.CurrentCulture,Resources.AsmRefSchemaMsg,InstallReferenceGuid.GetGuidSchemeName(instRef.GuidScheme));
sb.Append(" ");
sb.AppendFormat(CultureInfo.CurrentCulture,Resources.AsmRefIDMsg,instRef.Identifier);
sb.Append(" ");
sb.AppendFormat(CultureInfo.CurrentCulture,Resources.AsmRefDescriptionMsg,instRef.NonCanonicalData);
collection.Add(assemblyName, sb.ToString());
}
else
{
// keine InstallReferenz gefunden, NULL anfügen
collection.Add(assemblyName, null);
}
return collection;
}Als Container welcher die Namen der Assemblies im GAC enthält, wird eine ReadOnlyCollection vom Typ string verwendet. Da die Erzeugung einer solche Kollektion bereits in der Klasse als öffentliche Methode vorhanden ist, wird diese einfach benutzt um eine Aufzählung der benötigten Assemblies zu erhalten. Nun braucht nur noch durch diese Aufzählung iteriert zu werden, und der jeweilige Rückgabewert wird an eine der beiden Methoden übergeben.
Um zu ermitteln ob die parallele Methode überhaupt einen Vorteil bringt, muss zunächst einmal die benötigte Zeit der sequenziellen Verwendung ermittelt werden. Wie schon für gacutil.exe wurde auch hier der Mittelwert aus fünf Durchgängen ermittelt. Wie bereits oben beschrieben ist dies nur eine einfache Iteration. Als einfachste Möglichkeit bietet sich hier eine foreach Schleife an.
// Collection mit Assemblies füllen die mit
// assemblyName übereinstimmen
assemblyList = GetAssemblies(assemblyName);
foreach (string asmName in assemblyList)
{
// je nach verwendeter Methode auskommentieren
//AddToCollection(asmName);
assemblyCollection.Add(GetNameValuePair(asmName));
}Als Mittelwerte habe ich für die Methode AddToCollection(String) 43,338 Sekunden und für die Methode GetNameValuePair(String) 43,678 Sekunden ermittelt. Für 676 angefügte Elemente ist die Differenz von ca. 0,3 Sekunden zu vernachlässigen. Es zeigte mir aber auch, dass es faktisch keinen Unterschied bedeutet ob nun das Schlüssel-Wert Paar als zwei Zeichenfolgen oder als NameValueCollection angefügt werden.
Vor dem Erscheinen der Juni 2008 CTP des ParallelFX hatte ich bereits die Dezember 2007 CTP der selbigen genutzt. Ich wollte natürlich wissen ob sich in Sachen Performance etwas getan hatte. Also habe ich als erstes die Dezember 2007 CTP zum Testen der Parallelisierung verwendet. Als Container für die Assemblynamen wurde die gleiche ReadOnlyCollection wie in der sequenziellen Methode verwendet. Zum parallelen iterieren verwende ich die statische foreach Methode der Parallel Klasse. Hier das Code-Konstrukt der verwendeten Methode:
// Collection mit den Namen aller Assemblies im GAC erzeugen
assemblyList = GetAssemblies(null);
var exceptions = new ConcurrentStack<Exception>();
Parallel.ForEach<string>(
assemblyList,
delegate(string asmName)
{
try
{
// je nach verwendeter Methode auskommentieren
//AddToCollection(asmName);
assemblyCollection.Add(GetNameValuePair(asmName));
}
// zum testen werden die Ausnahmen nicht unterschieden
catch (Exception ex)
{
exceptions.Push(ex);
}
});
// prüfen ob Ausnahmen abgefangen wurden
if (!exceptions.IsEmpty)
{
throw new AggregateException(exceptions);
}Die Verwendung der Parallel-Klasse aus der Dezember 2007 CTP ergab für die Methode AddToCollection(String) einen Mittelwert von 39,316 Sekunden und für die Methode GetNameValuePair(String) 38,495. Beide Methoden brachten einen Geschwindigkeitsvorteil von rund 4 Sekunden. Wie ich jedoch Eingangs darauf hingewiesen habe, eignen sich Aufgaben mit intensiven E/A-Operationen nicht besonders zur Parallelisierung.
Um zu sehen was sich in der Juni 2008 CTP des ParallelFX getan hatte, wiederholte ich den Test mit der Parallel-Klasse aus der neueren CTP mit folgendem Ergebnis: die Methode AddToCollection(String) benötigte nur noch 36,495 Sekunden im Mittel. Auch die zweite Methode GetNameValuePair(String) war mit 36,662 im Mittel deutlich schneller geworden. Aus den anfänglichen rund 4 Sekunden Differenz zur sequenziellen Verarbeitung waren mittlerweile rund 7 Sekunden geworden. Bei rund 43,5 Sekunden als Basis ergeben die rund 36,5 Sekunden doch einen Vorteil von rund 16%.
Ich habe auf einen direkten Vergleich mit dem Ergebnis der gacutil.exe bewusst verzichtet da sie weder in verwaltetem Code erstellt wurde, noch ich irgendwelche Kenntnisse über die Implementierung der Fusion-API in der gacutil.exe besitze. Ich habe die Kennwerte der gacutil.exe lediglich ermittelt um einen Wert zu haben an dem ich mich orientieren kann und um die Anzahl der zu erwartenden Elemente zu kennen. Was hilft die beste Methode, wenn sie falsche Ergebnisse, in meinem Fall die Anzahl der Assemblies, liefert.
In der Zusammenfassung ergibt sich für mich folgendes Bild:
- Als Aufgabe eine Operation die sich nur sehr bedingt zur Parallelisierung eignet.
- Ohne nennenswerten Mehraufwand eine parallele Lösung gefunden.
- Einen Geschwindigkeitsvorteil von rund 16% erzielt, mit einer CTP welche mit Sicherheit noch nicht optimiert ist.
Ob nun in Zukunft vermehrt von der Parallelisierung Gebrauch gemacht wird? Ich weis es nicht. Von meiner Warte aus kann ich nur sagen: Mit dem Parallel-Erweiterungen hat uns das ParallelFX-Team ein einfach zu handhabendes und sehr wirkungsvolles Werkzeug zur Verfügung gestellt. Falls jemand einmal Probleme mit der Parallel-Bibliothek haben sollte helfen die Mitglieder des Teams gerne im MSDN-Forum. Ich kann aus eigener Erfahrung sagen, dass mir dort schnell und kompetent geholfen wurde.
Ob sich nun der einzelne mehr mit der Parallelisierung seiner Aufgaben auseinandersetzt bleibt ihm selbst überlassen. Ich werde es auf jeden Fall tun.
Wenn ihnen der Artikel gefallen hat oder er für sie hilfreich war, bitte "kicken" sie ihn.
