Die "dunkle" Seite der PowerShell

Bei der PowerShell ist praktisch alles ein Objekt. Was für Adhoc-Abfragen und die Weitergabe im Prinzip beliebiger Daten enorm praktisch ist, besitzt leider einen deutlichen Nachteil: Treten Objekte in großer Stückzahl auf, belegen sie sehr viel Arbeitsspeicher, der von der CLR (Common Language Runtime) des .NET Frameworks verwaltet wird, was leider sehr lange dauern kann. Im PowerShell-Alltag fällt dies im Allgemeinen nicht auf, doch sobald z.B. größere Dateien verarbeiten werden, kann sich dieser Umstand (sehr) negativ bemerkbar machen.

Die folgende Funktion liest eine CSV-Datei über eine StreamReader-Klasse (aus dem Namespace System.IO) zeilenweise ein und legt für jede Zeile per Select-Object ein neues Objekt an, das einige Spaltenwerte als Properties erhält, und fügt das Objekt in eine zuvor angelegte HashTable ($CSVNamen) ein. Alles in allem eine überschaubare Angelegenheit. Ich habe absichtlich auf Import-CSV verzichtet, da ich nicht den vermeintlichen „Overhead“ haben wollte, der durch den Umstand einhergeht, dass dieses Cmdlet pro Zeile der CSV-Datei immer ein Objekt angelegt (tatsächlich ist es aber bei Dateien kleiner bis mittlerer Größe die schnellste Variante).

Während das Einlesen von kleinen CSV-Dateien (bis zu 100.000 Zeilen) mit der Funktion noch relativ schnell geht, dauert das Einlesen einer CSV-Datei mit 500.000 Zeilen mehr als 10 Stunden (!) – und das auf einem Notebook mit 3 GByte RAM. Die Zeitmessung habe ich mit der praktischen StopWatch-Klasse durchgeführt, die das Ergebnis der Messung z.B. als TimeSpan-Objekt zurückgibt. Entweder habe ich in der Funktion etwas grundsätzliches falsch gemacht, oder die interne Speicherverwaltung des .NET Framework, auf dem die PowerShell bekanntlich aufsetzt, wirkt sich tatsächlich so ungünstig aus.

Die besagte Funktion sieht wie folgt aus:

function Get-CSVContent { param([string][parameter(mandatory=$true)]$CSVPfad) $Reg = "^(?<Nr>[0-9]{5}),(?<Vorname>\w+),(?<Nachname>[\w.]+),(?<Abteilung>\w+),(?<EMail>[\w.]+@\w+\.de)" $Sr = new-object System.IO.StreamReader $CSVPfad $Global:CSVNamen = @{} # Kopfzeile separat lesen $Sr.ReadLine() | out-null $StopUhr = new-object System.Diagnostics.StopWatch $StopUhr.Start() while (!$Sr.EndOfStream) { $Zeile = $Sr.ReadLine() $Zeile -match $Reg | Out-Null $Global:CSVNamen[$Matches.Abteilung] += @(0 | Select-Object @{Name="Nr";Expression={$Matches.Nr}}, ` @{Name="Vorname";Expression={$Matches.Vorname}}, ` @{Name="Nachname";Expression={$Matches.Nachname}}) $Anzahl++ if( $Anzahl % 5000 -eq 0) { "Bearbeite Satz $Anzahl... - freier Arbeitsspeicher: $([System.GC]::GetTotalMemory(0)"} } $StopUhr.Stop() write-host -fore green ` "$Anzahl Zeilen in $("{0:00}" -f $StopUhr.Elapsed.Hours):$("{0:00}" -f $StopUhr.Elapsed.Minutes):$("{0:00}" -f $StopUhr.Elapsed.Seconds) Minuten verarbeitet..." $Sr.Close() $Sr.Dispose() }

Ich fürchte, dass sich daran nichts ändern lässt. Besser wird es erst, wenn die PowerShell „Multicore-fähig“ wird und ein Prozess automatisch auf die verschiedenen Kernen der CPU verteilt wird. Da dieses Feature beim kommenden .NET Framework 4.0 dabei sein wird ist es nicht unwahrscheinlich, dass eine „PowerShell 3.0“ dies bieten wird.

PowerShellLangsamerProzess
(kaum zu glauben, aber leider wahr – das Einlesen von 500.000 Zeilen einer CSV-Datei dauert über 10 Stunden)

Eine Antwort zu “Die "dunkle" Seite der PowerShell

  1. Richtig unheimlich, diese dunkle Seite!

Hinterlasse einen Kommentar