PowerShell: Performance-Benchmark für achronologische Datei-Ausgabe

Neulich im TechNet-Forum: Jemand möchte informationen aus einem Script in eine Textdatei ausgeben, aber neuere Daten nicht anhängen, sondern an den Anfang schreiben. So etwas ist freilich normalerweise nicht vorgesehen, und daher erging im Thread sehr schnell die Empfehlung, den Content jeweils einzulesen, dann die neue Zeile und anschließend den zwischengespeicherten Content in die Datei zu schreiben. So etwas ist ein typischer Fall von „vermutlich ist die Performance miserabel“, daher wollte ich es mal untersuchen.

Die Versuchsanordnung

Alle Tests werden auf physischer Hardware (i5 mit 16 GB RAM und SSD, Windows 10 1909, Defender für den Test ausgeschaltet, keine weiteren Prozesse aktiv außer PowerShell) ausgeführt. Da ich heute nicht sehr kreativ bin, nehme ich für das Erstellen der Datei-Elementen einfach mal einen fortlaufenden Counter und gebe ihn Zeile für Zeile, auf 80 Zeichen mit Nullen aufgepumpt, aus. Zunächst einmal die Baseline (10.000 Zeilen):

$nmembers = 10000
$timer = [System.Diagnostics.Stopwatch]::StartNew()
for ($i = 0; $i -lt $nmembers; $i++) {
    $item = $i.ToString().PadLeft(80,"0")
}
$timer.Elapsed.TotalMilliseconds

OK, mit im Schnitt 10-12 Millisekunden dürfte das für die nun folgenden Versuche nicht allzusehr ins Gewicht haben. Schauen wir uns einmal die im Forum vorgeschlagene „PowerShell-mäßige“ Vorgehensweise an:

$file = "C:\temp\filetest\strings.txt"
$nmembers = 10000
$timer = [System.Diagnostics.Stopwatch]::StartNew()
for ($i = 0; $i -lt $nmembers; $i++) {
    $item = $i.ToString().PadLeft(80,"0")
    if (Test-Path $file) {
        $array = Get-Content -Path $file -Encoding UTF8
    } else {
        $array = @()
    }
    $item | Set-Content -Path $file -Encoding UTF8
    $array | Add-Content -Path $file -Encoding UTF8
}
$timer.Elapsed.TotalMilliseconds

Wow. Mit „miserabel“ ist das noch recht wohlwollend umschrieben. 1.080 Sekunden für 801 KB Textdatei? Das sind 18 Minuten – es muss schneller gehen.

Dateioperationen optimieren

Der erste Griff geht natürlich in Richtung „aus PowerShell ausbrechen und .NET-Methoden verwenden“. Wir behalten die Logik im Skript bei und ersetzen *-Content durch StreamReader- und StreamWriter-Methoden (ohne dabei die Zeilenumbrüche zu „verlieren“):

$file = "C:\temp\filetest\strings.txt"
$nmembers = 10000
$timer = [System.Diagnostics.Stopwatch]::StartNew()
for ($i = 0; $i -lt $nmembers; $i++) {
    $item = $i.ToString().PadLeft(80,"0")
    $array = New-Object System.Collections.ArrayList
    if ([System.IO.File]::Exists($file)) {
        $sread = [System.IO.StreamReader]::new($file,[System.Text.UTF8Encoding]::UTF8)
        $null = $array.Add($item)
        while($line = $sread.ReadLine()) {
            $null = $array.Add($line)
        }
        $sread.Close()
    }
    $stream = [System.IO.StreamWriter]::new($file,$false,[System.Text.UTF8Encoding]::UTF8)
    for ($i = 0; $i -lt $array.Count; $i++) {
        $stream.WriteLine($array[$i])
    }
    $stream.Close()
}
$timer.Elapsed.TotalMilliseconds

Schon viel besser – das Skript wird in 119 Sekunden fertig, Beschleunigung nahezu um Faktor 10! Doch sind die vielen Schreib- und Lese-Operationen nicht optimal.

Dateioperationen eliminieren

Es liegt also der Gedanke nahe, die Datei zuerst so zu schreiben, wie vom Hersteller vorgesehen, und dann in einem Rutsch umzudrehen. Der Autor des Threads hat zwar darauf hingewiesen, dass er die Datei über längere Zeiträume kontinuierlich schreibt, aber man kann sie ja dabei trotzdem einlesen – auf einem Zwischenstand halt.

Mit PowerShell-Mitteln würde das dann so aussehen:

$file = "C:\temp\filetest\strings.txt"
$nmembers = 10000
$timer = [System.Diagnostics.Stopwatch]::StartNew()
"" | Set-Content -Path $file -Encoding UTF8
for ($i = 0; $i -lt $nmembers; $i++) {
    $item = $i.ToString().PadLeft(80,"0")
    $item | Add-Content -Path $file -Encoding UTF8
}
$timer.Elapsed.TotalMilliseconds
$array = Get-Content -Path $file -Encoding UTF8
"" | Set-Content -Path $file -Encoding UTF8
for ($i = $array.Count -1; $i -ge 0; $i--) {
    $array[$i] | Add-Content -Path $file -Encoding UTF8
}
$timer.Elapsed.TotalMilliseconds

und in knapp 11 Sekunden fertig. Bingo! Beschleunigung um den Faktor 100, zumindest bei dieser Anzahl Zeilen!

Greift man nun zu den Stream*-Methoden aus dem .NET-Arsenal, so liefert

$file = "C:\temp\filetest\strings.txt"
$nmembers = 10000
$timer = [System.Diagnostics.Stopwatch]::StartNew()
$stream = [System.IO.StreamWriter]::new($file,$false,[System.Text.UTF8Encoding]::UTF8)
for ($i = 0; $i -lt $nmembers; $i++) {
    $item = $i.ToString().PadLeft(80,"0")
    $stream.WriteLine($item)
}
$stream.Close()
$sread = [System.IO.StreamReader]::new($file,[System.Text.UTF8Encoding]::UTF8)
$array = New-Object System.Collections.ArrayList
while($line = $sread.ReadLine()) {
    $null = $array.Add($line)
}
$sread.Close()
$stream = [System.IO.StreamWriter]::new($file,$false,[System.Text.UTF8Encoding]::UTF8)
for ($i = $array.Count -1; $i -ge 0; $i--) {
    $stream.WriteLine($array[$i])
}
$stream.Close()
$timer.Elapsed.TotalMilliseconds 

das Ergebnis bereits in unter 40 Millisekunden! Das ist eine Beschleunigung gegenüber dem ersten Versuch um den Faktor 27.000!

Happy outputting!

 

1 Trackback / Pingback

  1. PowerShell: Achronologische Ausgabe unter Linux – Evgenij Smirnov – IT Pro aus Berlin

Antworten

Deine E-Mail-Adresse wird nicht veröffentlicht.


*


Diese Website verwendet Akismet, um Spam zu reduzieren. Erfahre mehr darüber, wie deine Kommentardaten verarbeitet werden.