PowerShell PowerShell-Performance-Tipps

DPXone

Lt. Junior Grade
Dabei seit
Mai 2009
Beiträge
502
[Der Beitrag ist in Bearbeitung und beinhaltet nicht alle Tipps von Anfang an.]

Hey Zusammen,

ich möchte den Leuten, die mit PowerShell zu tun haben und PowerShell-Anfänger sind, hier mal eine Sammlung meiner bisher gesammelten Erfahrungen bezüglich Performance in PowerShell mittteilen.]

Der Guide ist extra im (hoffentlich) leichten Niveau gehalten, damit auch Anfänger damit etwas anfangen können.

Bitte stellt eure Fragen zum Guide hier. Alles was unklar ist wird im Nachgang hier im Guide geändert, damit Nachfolger hoffentlich weniger Probleme haben.
Sollte euch das Englisch in den Skripten stören, das mach ich leider generell aufgrund Internationalität ;) . Wir klären das hier aber gerne. Meldet euch einfach.

Ich hoffe, dass ich damit dem ein oder anderen helfen kann ;)
Bei Fragen, Anmerkungen oder weiteren Performance-Tipps --> Meldet euch.

Hinweis
Die Beispiel-Skripte sind in sogenannte Regionen "#region" eingeteilt.
(Regionen werden durch das Zeichen und Statement "#region" pro Zeile dargestellt)

Voraussetzung für meine Aufführungen, Bemerkungen, Scripts und Ergebnisse ist PowerShell 5.1, da es bis dato doch sehr viel Performance-Verbesserungen bei diversen Ansätzen gegeben hat.
PowerShell 5.1 bekommt ihr durch das Windows Management Framework 5.1 (Kürzform: WMF):
https://www.microsoft.com/en-us/download/details.aspx?id=54616

Was neuere WMF-Versionen so bewirken können, hier ein Beispiel aus diesem Forum (siehe die Antwort an mich (@DPXone) in "Ergänzung (10. Juli 2017)" in Bezug auf Windows Management Framework 5.1 und Abschlusstext):
https://www.computerbase.de/forum/t...reren-einzelnen-zellen.1693664/#post-20248809

Vorteilhaft ist auch, dass ihr Englisch versteht, da ich die Skript-Beispiele in Englisch dokumentiere.

Die relevanten Code-Ausführen werden in den Beispielen mit Ausrufezeichen gekennzeichnet:
PowerShell:
#!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
# This is the execution we're looking at:

Hier steht der CODE CODE CODE CODE CODE CODE
#!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!


Wenn ihr nicht wisst, was ein "Pipeline-Input" ist:
Pipeline-Input ist die Übergabe von Werten von einem Befehl / einer Funktion zur Nächsten über eine Pipe.
Gekennzeichnet wird die Pipe durch das Zeichen "|" zwischen den Befehlen.
Beispiel:
PowerShell:
Get-ChildItem 'C:\' | Get-Content


PowerShell-Performance-Tipps:
  1. Verwendet keine Arrays ( $array= @() ), wenn dem Array ständig Werte hinzugefügt werden ($array+=$BlaBla)!

    --> Verwendent die Typen "ArrayList", "GenericList" oder eine direkte Zuweisung zu Variablen. (Siehe Beispiel unten)u
    Arrays, initialisiert durch $Array = @(), verwenden viele, weil sie es womöglich nicht anders von PowerShell kennen.
    Arrays besitzen eine feste Größe und sind unveränderlich (immutable), das heißt, wenn ihr das Array, wie oben erstmals initialisiert, dann besitzt es eine Größe von 0; direkte Änderungen unmöglich.

    Jedes Mal, wenn ihr dann via $Array+=$neuesItem (nennt sich in PowerShell "op_addition") etwas hinzufügt, dann wird das Item (der Wert den ihr hinzufügen möchtet) nicht direkt hinzugefügt, sondern es wird ein komplett neues Array erstellt, mit einer neuen Größe und alle Items des bisherigen Arrays werden rüber kopiert.
    Das frisst, je größer das Array wird, enorm an der Performance.


    Um das zu Veranschaulichen gibt's unten ein Beispiel und zusätzlich das Ergebnis, welches ingesamt vier Varianten aufzeigt, wie man Objekte und Werte in einer Variable sammeln kann.

    Im Beispiel unten wird nach jeder Ausführung auch die Ausführungszeit angezeigt.
    • Test 1 führt einen Pipeline-Input aus. Hierbei wird "Where-Object" (Alias: ?) verwendet (% / ForEach-Object bewirkt ein ähnliches Ergebnis!).
    • Test 2 nutzt die oben erwähnte Variante des Array mit op_addition aus, wobei das Array automatisch jedes Mal neu erstellt wird.
    • Test 3 nutzt das "ForEach"-Statement und nutzt eine ArrayList, um neue Werte hinzuzufügen.
    • Test 4 nutzt das "ForEach"-Statement und fügt die Werte im Nachgang direkt einer Variablen zu

    Info:
    "Write-Progress" bewirkt im Beispiel so gut wie keine Performance-Einbußen, wenn man es wie im Beispiel gezielt und ressourcen-schonend (=mit mathematischem modulo --> Zahl % Zahl = 0) einsetzt!

    Die Details zum performanten Umgang mit Write-Progress und Write-Host werden später erklärt.


    Performance Beispiel 1:
    PowerShell:
    $testInput = 1 .. 200000 # test numbers between 1 and 200.000    
    $progressMax = @($testInput).count # max count of above variable    
    $progressInterval = [math]::Ceiling($testInput.Count * 0.1) # Feedback in 10% steps for "write-progress". Executing it for each item has a very huge performance impact!     
    $stopwatch = [System.Diagnostics.Stopwatch]::new() # let's get the stopwatch started    
    
    
    CLS # clear the screen ...    
    write-host ("`r`n" * 5) # ... and add 5 empty lines    
    [gc]::Collect() # Collecing garbage, for a way of better performance measuring    
    
    
    #############################################################################################################################     
    #region TEST 1: Where-Object     
    #----------------------------------------------------------------------------------------------------------------------------     
    Write-Host 'TEST 1: Testing Where-Object' 
    Write-Host "`tDisadvantages:" 
    Write-Host "`t- Using Pipeline-Input (medium performance loss compared to the best approach)`r`n" 
    write-host "`tProcessing times:" 
    $stopwatch.Start() 
    
    $i = -1 # starting with less 1 as common to show write-progress bar at starting point and not just on first hit after 0     
    
    #!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!     
    # This is the execution we're looking at:     
    $testoutput = $testInput | ? { 
    	#!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!     
    	
    	#.......................................................................................................................     
    	#This is just for progress activity returns     
    	$i++ 
    	If ($i % $progressInterval -eq 0) { 
    		write-progress -Activity 'Processing "Where-Object"' -PercentComplete($i / $progressMax * 100) 
    		
    		If ($i -gt 0) { 
    			Write-Host ("`t`t{0,-10} to {1,-10} processed in: {2}" -f ($i - $progressInterval) ,($i) , $($stopwatch.Elapsed - $LastTime)) 
    		} 
    		$LastTime = $stopwatch.Elapsed 
    	} 
    	#.......................................................................................................................     
    	
    	
    	#!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!     
    	# This is the execution we're looking at:     
    	$_ % 3 -eq 0 
    	#!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!     
    } 
    
    $ElapsedTimeTest1 = $stopwatch.Elapsed 
    write-host "`r`n`tTotal time: $($ElapsedTimeTest1.ToString())" 
    #endregion   
    #############################################################################################################################     
    
    
    Write-Host ('*' * 50) 
    Write-Host ("`r`n" * 2) 
    [gc]::Collect() 
    
    
    #############################################################################################################################     
    #region TEST 2: ForEach with op_addition operation     
    #----------------------------------------------------------------------------------------------------------------------------     
    Write-Host 'TEST 2: Testing ForEach with op_addition for the immutable array / creating new arrays (most used)' 
    Write-Host "`tDisadvantages:" 
    Write-Host "`t- Using an Array although it's of a fixed size (very huge performance loss compared to the best approach)`r`n" 
    write-host "`tProcessing times:" 
    $stopwatch.Restart() 
    
    $i = -1 # starting with less 1 as common to show write-progress bar at starting point and not just on first hit after 0     
    
    #!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!     
    # This is the execution we're looking at:     
    $testoutput2 = @() 
    Foreach ($item In $testInput) { 
    	#!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!     
    	
    	#.......................................................................................................................     
    	#This is just for progress activity returns     
    	$i++ 
    	If ($i % $progressInterval -eq 0) { 
    		write-progress -Activity 'Processing "ForEach with op_addition for the immutable array" (See how the perfomance decreases the bigger the array gets)' -PercentComplete($i / $progressMax * 100) 
    		
    		If ($i -gt 0) { 
    			Write-Host ("`t`t{0,-10} to {1,-10} processed in: {2}" -f ($i - $progressInterval) ,($i) , $($stopwatch.Elapsed - $LastTime)) 
    		} 
    		$LastTime = $stopwatch.Elapsed 
    	} 
    	#.......................................................................................................................     
    	
    	
    	#!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!     
    	# This is the execution we're looking at:     
    	If ($item % 3 -eq 0) { 
    		$testoutput2+= $item 
    	} 
    	#!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!     
    } 
    
    $ElapsedTimeTest2 = $stopwatch.Elapsed 
    write-host "`r`n`tTotal time: $($ElapsedTimeTest2.ToString())" 
    #endregion   
    #############################################################################################################################     
    
    
    Write-Host ('*' * 50) 
    Write-Host ("`r`n" * 2) 
    [gc]::Collect() 
    
    
    #############################################################################################################################     
    #region TEST 3: ForEach with ArrayList     
    #----------------------------------------------------------------------------------------------------------------------------     
    Write-Host 'TEST 3: Testing ForEach with ArrayList' 
    Write-Host "`tAdvantages: " 
    Write-Host "`t- Using ArrayList (very low performance loss compared to the best approach)`r`n" 
    write-host "`tProcessing times:" 
    $stopwatch.Restart() 
    
    $i = -1 # starting with less 1 as common to show write-progress bar at starting point and not just on first hit after 0     
    
    #!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!     
    # This is the execution we're looking at:     
    $testoutput3 = [System.Collections.ArrayList]::new() 
    Foreach ($item In $testInput) { 
    	#!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!     
    	
    	#.......................................................................................................................     
    	#This is just for progress activity returns     
    	$i++ 
    	If ($i % $progressInterval -eq 0) { 
    		write-progress -Activity 'Processing "Testing ForEach with ArrayList"' -PercentComplete($i / $progressMax * 100) 
    		
    		If ($i -gt 0) { 
    			Write-Host ("`t`t{0,-10} to {1,-10} processed in: {2}" -f ($i - $progressInterval) ,($i) , $($stopwatch.Elapsed - $LastTime)) 
    		} 
    		$LastTime = $stopwatch.Elapsed 
    	} 
    	#.......................................................................................................................     
    	
    	
    	#!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!     
    	# This is the execution we're looking at:     
    	If ($item % 3 -eq 0) { 
    		[void] $testoutput3.Add($item) 
    	} 
    	#!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!     
    } 
    
    $ElapsedTimeTest3 = $stopwatch.Elapsed 
    write-host "`r`n`tTotal time: $($ElapsedTimeTest3.ToString())" 
    #endregion   
    #############################################################################################################################     
    
    
    Write-Host ('*' * 50) 
    Write-Host ("`r`n" * 2) 
    [gc]::Collect() 
    
    
    #############################################################################################################################     
    #region TEST 4: ForEach with direct variable association     
    #----------------------------------------------------------------------------------------------------------------------------     
    Write-Host 'TEST 4: Testing ForEach with direct variable association' 
    Write-Host "`tAdvantages:" 
    Write-Host "`t- Using ForEach with direct association of output to variable (perfect performance)`r`n" 
    write-host "`tProcessing times:" 
    $stopwatch.Restart() 
    
    $i = -1 # starting with less 1 as common to show write-progress bar at starting point and not on first hit after 0     
    
    #!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!     
    # This is the execution we're looking at:     
    $testoutput4 = Foreach ($item In $testInput) { 
    	#!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!     
    	
    	#.......................................................................................................................     
    	#This is just for progress activity returns     
    	$i++ 
    	If ($i % $progressInterval -eq 0) { 
    		write-progress -Activity 'Processing "ForEach with direct variable association"' -PercentComplete($i / $progressMax * 100) 
    		
    		If ($i -gt 0) { 
    			Write-Host ("`t`t{0,-10} to {1,-10} processed in: {2}" -f ($i - $progressInterval) ,($i) , $($stopwatch.Elapsed - $LastTime)) 
    		} 
    		$LastTime = $stopwatch.Elapsed 
    	} 
    	#.......................................................................................................................     
    	
    	
    	#!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!     
    	# This is the execution we're looking at:     
    	If ($item % 3 -eq 0) { 
    		$item 
    	} 
    	#!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!     
    } 
    
    $ElapsedTimeTest4 = $stopwatch.Elapsed 
    write-host "`r`n`tTotal time: $($ElapsedTimeTest4.ToString())" 
    #endregion   
    #############################################################################################################################     
    
    
    Write-Host ('*' * 50) 
    Write-Host ("`r`n" * 2) 
    [gc]::Collect() 
    
    
    #############################################################################################################################     
    #region Summary     
    #----------------------------------------------------------------------------------------------------------------------------     
    
    write-host ("#" * 70) 
    write-host 'Summary:' 
    write-host ("-" * 70) 
    Write-Host '' 
    $BestExecutionTime = $ElapsedTimeTest1 , $ElapsedTimeTest2 , $ElapsedTimeTest3 , $ElapsedTimeTest4 | Measure-Object -Minimum 
    
    
    write-host 'Test1:' 
    $result = [math]::Round($ElapsedTimeTest1.Ticks / $BestExecutionTime.Minimum.Ticks , 2) - 1 
    If ($result -eq 0) { 
    	write-host "`t*** BEST ***" 
    } Else { 
    	write-host "`t$result times slower than best" 
    } 
    
    
    Write-Host "`r`n" 
    
    
    write-host 'Test2:' 
    $result = [math]::Round($ElapsedTimeTest2.Ticks / $BestExecutionTime.Minimum.Ticks , 2) - 1 
    If ($result -eq 0) { 
    	write-host "`t*** BEST ***" 
    } Else { 
    	write-host "`t$result times slower than best" 
    } 
    
    
    Write-Host "`r`n" 
    
    
    write-host 'Test3:' 
    $result = [math]::Round($ElapsedTimeTest3.Ticks / $BestExecutionTime.Minimum.Ticks , 2) - 1 
    If ($result -eq 0) { 
    	write-host "`t*** BEST ***" 
    } Else { 
    	write-host "`t$result times slower than best" 
    } 
    
    
    Write-Host "`r`n" 
    
    
    write-host 'Test4:' 
    $result = [math]::Round($ElapsedTimeTest4.Ticks / $BestExecutionTime.Minimum.Ticks , 2) - 1 
    If ($result -eq 0) { 
    	write-host "`t*** BEST ***" 
    } Else { 
    	write-host "`t$result times slower than best" 
    } 
    
    Write-Host "`r`n" 
    write-host ("#" * 70) 
    #endregion  
    #############################################################################################################################


    Ergebnis (Performance Beispiel 1): (Zeiten sind im Format: [STUNDE]:[MINUTE]:[SEKUNDE].[MILLISEKUNDE]
    Code:
    TEST 1: Testing Where-Object
    	Disadvantages:
    	- Using Pipeline-Input (medium performance loss compared to the best approach)
    
    	Processing times:
    		0          to 20000      processed in: 00:00:00.2148211
    		20000      to 40000      processed in: 00:00:00.1917197
    		40000      to 60000      processed in: 00:00:00.1860353
    		60000      to 80000      processed in: 00:00:00.1786461
    		80000      to 100000     processed in: 00:00:00.1769503
    		100000     to 120000     processed in: 00:00:00.1749551
    		120000     to 140000     processed in: 00:00:00.1798344
    		140000     to 160000     processed in: 00:00:00.1899303
    		160000     to 180000     processed in: 00:00:00.1769276
    
    	Total time: 00:00:01.8778313
    **************************************************
    
    
    
    TEST 2: Testing ForEach with op_addition for the immutable array / creating new arrays (most used)
    	Disadvantages:
    	- Using an Array although it's of a fixed size (very huge performance loss compared to the best approach)
    
    	Processing times:
    		0          to 20000      processed in: 00:00:00.8225148
    		20000      to 40000      processed in: 00:00:02.6714964
    		40000      to 60000      processed in: 00:00:04.4622587
    		60000      to 80000      processed in: 00:00:06.1615622
    		80000      to 100000     processed in: 00:00:07.7802078
    		100000     to 120000     processed in: 00:00:10.0054058
    		120000     to 140000     processed in: 00:00:11.9739167
    		140000     to 160000     processed in: 00:00:14.0041214
    		160000     to 180000     processed in: 00:00:14.9395773
    
    	Total time: 00:01:29.0301052
    **************************************************
    
    
    
    TEST 3: Testing ForEach with ArrayList
    	Advantages: 
    	- Using ArrayList (very low performance loss compared to the best approach)
    
    	Processing times:
    		0          to 20000      processed in: 00:00:00.0498273
    		20000      to 40000      processed in: 00:00:00.0398220
    		40000      to 60000      processed in: 00:00:00.0418950
    		60000      to 80000      processed in: 00:00:00.0420203
    		80000      to 100000     processed in: 00:00:00.0402917
    		100000     to 120000     processed in: 00:00:00.0396671
    		120000     to 140000     processed in: 00:00:00.0382014
    		140000     to 160000     processed in: 00:00:00.0381381
    		160000     to 180000     processed in: 00:00:00.0404453
    
    	Total time: 00:00:00.4241538
    **************************************************
    
    
    
    TEST 4: Testing ForEach with direct variable association
    	Advantages:
    	- Using ForEach with direct association of output to variable (perfect performance)
    
    	Processing times:
    		0          to 20000      processed in: 00:00:00.0484032
    		20000      to 40000      processed in: 00:00:00.0378398
    		40000      to 60000      processed in: 00:00:00.0369516
    		60000      to 80000      processed in: 00:00:00.0390416
    		80000      to 100000     processed in: 00:00:00.0380139
    		100000     to 120000     processed in: 00:00:00.0377628
    		120000     to 140000     processed in: 00:00:00.0382655
    		140000     to 160000     processed in: 00:00:00.0366415
    		160000     to 180000     processed in: 00:00:00.0375807
    
    	Total time: 00:00:00.3996698
    **************************************************
    
    
    
    ######################################################################
    Summary:
    ----------------------------------------------------------------------
    
    Test1:
    	3.7 times slower than best
    
    
    Test2:
    	221.76 times slower than best
    
    
    Test3:
    	0.0600000000000001 times slower than best
    
    
    Test4:
    	*** BEST ***
    
    
    ######################################################################

    Wie ihr seht, dauert die Ausführung durch die allseits bekannte Variante $Array+=$neuesItem im Beispiel ingesamt mehr als 200-mal länger, als wenn man ForEach in Verbindung mit einer direkten Zuweisung zu einer Variablen verwendet oder alternativ auf eine ArrayList ausweicht, ohne jetzt weiter auf die für Programmierer bessere Variante GenericList mit Typsierung einzugehen
    (Derjenige, der die Generic List "[System.Collections.Generic.List[<Type>]]" kennt, ist bei diesem Performance-Tipp eh falsch.)

    Hinweis:
    Wenn ihr die Variante mit der ArrayList verwenden wollt, dann beim Hinzufügen von neuen Werten immer ein [void] vorne dranhängen (wie oben im Beispiel), da ihr sonst fortlaufend in der Konsole die aktuell hinzugefügte ID ausgebt, was mitunter auch wieder Performance-Probleme verursacht.


  2. Schreibt nicht via Out-File oder Set-Content über die Pipeline in Dateien!

    --> Verwendent eine vorgelagerte Konvertierung zum Typ "single" String (Siehe Beispiel unten]
    )

    Variablen oder die Ausgabe von CmdLets, Funktionen, etc direkt in eine Datei zu schreiben, in dem man die Pipeline nutzt, verursacht enorme Performance-Einbußen.

    Hintergrund ist, dass beim Pipeline-Input für jede "Zeile" des zugeführten Objekts folgende Operationen auf der Festplatte passieren:
    1. Öffnen
    2. Schreiben
    3. Schließen

    Das passiert durch das "Stream"-ing.
    Um das zu verhindern ist es zu empfehlen die finale Ausgabe erstmal in ein vollständiges Objekt vom Typ String zu konvertieren.

    Hier hängt es jedoch stark davon ab, was man am Ende denn in der Datei haben möchte
    ... simpler Text .... CSV .... XML ???

    Egal was auch erreicht werden möchte, die Ausgabe in die Datei sollte nicht über die Pipeline erfolgen, sonst passiert der oben beschriebene Prozess.
    Ausnahmen bestätigen wie immer aber auch die Regeln:
    Kritische Logs sollten von dieser Variante ausgenommen werden, da es mit meiner Variante natürlich keine "Zwischenstände" gibt, sollte ein System mal abrauchen, obwohl die Datei noch nicht fertig geschrieben wurde.

    Performance Beispiel 2:
    PowerShell:
    $testInput = 1 .. 2000000 
    $stopwatch = [System.Diagnostics.Stopwatch]::new() 
    
    CLS 
    
    ############################################################################################################################# 
    #region Initialize functions for the test 
    #---------------------------------------------------------------------------------------------------------------------------- 
    
    # ConvertTo-String is just for outputting a single text of objects 
    # When outputting delimited content with multiple properties, 'ConvertTo-CSV' has to be used! 
    # Hint: 'ConvertTo-CSV is much faster than 'Out-String" but can't be used for objects with less than 2 properties! 
    Function ConvertTo-String { # Very much faster than 'Out-String' for outputting just single text (but not objects with more than 1 properties!)) ;Use: $out = "ConvertTo-String -InputObject $test" and not the pipeline 
    	[CmdletBinding()] 
    	Param (
    		[Parameter(Mandatory = $true , ValueFromPipeline = $true , Position = 0)] 
    		$InputObject 
    	) 
    	
    	Begin { 
    		$StringBuilder = [System.Text.StringBuilder]::new() 
    	} 
    	
    	Process { 
    		Foreach ($item In $InputObject) { 
    			[void] $StringBuilder.AppendLine($item -join "`t") 
    		} 
    	} 
    	
    	End { 
    		$StringBuilder.ToString() 
    	} 
    } 
    
    Function Get-RandomTextFilePath { 
    	Do { 
    		$RandomTextFilePath = "$($env:Temp)\$([System.IO.Path]::GetRandomFileName()).txt" 
    	} Until (-not(Test-Path $RandomTextFilePath -PathType Leaf)) 
    	
    	$RandomTextFilePath 
    } 
    ############################################################################################################################# 
    
    
    
    [gc]::Collect() 
    
    
    
    ############################################################################################################################# 
    #region TEST 1: Set-Content with Pipeline-Input 
    #---------------------------------------------------------------------------------------------------------------------------- 
    $TestFilePath = Get-RandomTextFilePath 
    Write-Host 'TEST 1: Testing Set-Content with Pipeline-Input' 
    Write-Host "`tOutput file path: $TestFilePath" 
    
    $stopwatch.Start() 
    
    #!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! 
    # This is the execution we're looking at: 
    $testInput | Set-Content -Path $TestFilePath -Encoding UTF8 -Force 
    #!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! 
    
    $ElapsedTimeTest1 = $stopwatch.Elapsed 
    write-host "`r`n`tTotal time: $($ElapsedTimeTest1.ToString())" 
    ############################################################################################################################# 
    
    
    
    Write-Host ('*' * 50) 
    Write-Host ("`r`n" * 2) 
    [gc]::Collect() 
    
    
    
    ############################################################################################################################# 
    #region TEST 2: Set-Content withOUT Pipeline-Input 
    #---------------------------------------------------------------------------------------------------------------------------- 
    $TestFilePath = Get-RandomTextFilePath 
    Write-Host 'TEST 2: Testing Set-Content withOUT Pipeline-Input' 
    Write-Host "`tOutput file path: $TestFilePath" 
    
    $stopwatch.Restart() 
    
    #!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! 
    # This is the execution we're looking at: 
    $outString = ConvertTo-String $testInput # Converting the INT array to string with custom function 
    Set-Content -Value $outString -Path $TestFilePath -Encoding UTF8 -Force 
    #!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! 
    
    $ElapsedTimeTest2 = $stopwatch.Elapsed 
    write-host "`r`n`tTotal time: $($ElapsedTimeTest2.ToString())" 
    ############################################################################################################################# 
    
    
    Write-Host ('*' * 50) 
    Write-Host ("`r`n" * 2) 
    
    
    ############################################################################################################################# 
    #region Summary 
    #---------------------------------------------------------------------------------------------------------------------------- 
    write-host ("#" * 70) 
    write-host 'Summary:' 
    write-host ("-" * 70) 
    Write-Host '' 
    
    
    $BestExecutionTime = $ElapsedTimeTest1 , $ElapsedTimeTest2 | Measure-Object -Minimum 
    
    
    write-host 'Test1:' 
    $result = [math]::Round($ElapsedTimeTest1.Ticks / $BestExecutionTime.Minimum.Ticks , 2) - 1 
    If ($result -eq 0) { 
    	write-host "`t*** BEST ***" 
    } Else { 
    	write-host "`t$result times slower than best" 
    } 
    
    
    Write-Host "`r`n" 
    
    
    write-host 'Test2:' 
    $result = [math]::Round($ElapsedTimeTest2.Ticks / $BestExecutionTime.Minimum.Ticks , 2) - 1 
    If ($result -eq 0) { 
    	write-host "`t*** BEST ***" 
    } Else { 
    	write-host "`t$result times slower than best" 
    } 
    
    Write-Host "`r`n" 
    write-host ("#" * 70) 
    #############################################################################################################################


    Ergebnis (Performance Beispiel 2): (Zeiten sind im Format: [STUNDE]:[MINUTE]:[SEKUNDE].[MILLISEKUNDE]
    Code:
    TEST 1: Testing Set-Content with Pipeline-Input
    	Output file path: C:\Users\scht33v\AppData\Local\Temp\eh2nobam.rcc.txt
    
    	Total time: 00:00:18.4654221
    **************************************************
    
    
    
    TEST 2: Testing Set-Content withOUT Pipeline-Input
    	Output file path: C:\Users\scht33v\AppData\Local\Temp\ekrquafk.amd.txt
    
    	Total time: 00:00:00.8063365
    **************************************************
    
    
    
    ######################################################################
    Summary:
    ----------------------------------------------------------------------
    
    Test1:
    	21.9 times slower than best
    
    
    Test2:
    	*** BEST ***
    
    
    ######################################################################



Großer Dank für Anmerkungen, weitere Verbesserungen, Korrekturen, Ideen und Co. geht an:
@RalphS
 
Zuletzt bearbeitet:
H

hroessler

Gast
Das verwenden von Arrays ist eh nur noch in speziellen Kontexten zu empfehlen. In der Regel sollte man Listen verwenden. Und wenn man Listen verwendet, dann doch bitte gleich die generische Variante List<T>. Diese ist (wenn wir schon bei Performance sind) performanter und zudem typsicherer. Die ArrayList ist doch schon seit .net 2.0 überholt...

greetz
hroessler
 

DPXone

Lt. Junior Grade
Ersteller dieses Themas
Dabei seit
Mai 2009
Beiträge
502
Das verwenden von Arrays ist eh nur noch in speziellen Kontexten zu empfehlen. In der Regel sollte man Listen verwenden. Und wenn man Listen verwendet, dann doch bitte gleich die generische Variante List<T>. Diese ist (wenn wir schon bei Performance sind) performanter und zudem typsicherer. Die ArrayList ist doch schon seit .net 2.0 überholt...

greetz
hroessler

ohne jetzt weiter auf die für Programmierer bessere Variante GenericList mit Typsierung einzugehen.

Deswegen hab ich das ja schon erwähnt ;)

PS: ArrayLists lassen sich in PowerShell als Alternative zu meinem genannten Thema (op_addition) halt schneller und leichter schreiben (insbesondere als Laie), als eine Generic List.
Performance-mäßig nehmen sich ArrayList und GenericList allerdings nichts!
Sie sind nur sicherer.
 
Zuletzt bearbeitet:

RalphS

Lt. Commander
Dabei seit
Feb. 2015
Beiträge
1.735
Naja, PS hat das Problem, daß Generics entweder unhandlich (Klassen) oder gar nicht verfügbar sind (Methoden).

Ein paar Dinge, die man berücksichtigen kann:

-- Nicht nur Arrays sind immutable, also unveränderlich. Strings sind das auch. Wenn man also mit Strings arbeitet und diese insbesondere ändern will, sollte man auf Alternativen ausweichen. Das gilt übrigens auch, wenn man Strings nach Strukturen durchsuchen will, also parsen will.

++ Besser: Statt String lieber Ienumerable<char> oder List<char> verwenden, bzw in PowerShell-Syntax
[system.collections.generic.list[char]].


-- PowerShell bietet die Möglichkeit, über Objekte mit foreach($obj in $collection) zu enumerieren oder $collection in Foreach-Object zu füttern.

++ In den meisten Fällen ist foreach($obj in $collection) schneller.

++ Allerdings kann PowerShell in Anlehnung an LINQ's Möglichkeiten Listen direkt verarbeiten. Dann spart man sich das ganz. Beispiel:
PowerShell:
# Dateiliste holen
[system.io.fileinfo[]]$filelist = Get-ChildItem -File -Path C:\
# $filelist hat jetzt eine unbekannte Anzahl von Dateiobjekten
# Wir wollen den Pfadnamen..
# Option A:
$filelist | % { $_.FullName}
# Option B:
foreach($obj in $filelist ) {$obj.FullName}
# Option C:
$filelist | Select -ExpandProperty FullName
# Option D:
$filelist.FullName
Option D funktioniert nur in PowerShell und macht dasselbe wie alle anderen Optionen auch, es liefert ein Array of strings mit den Pfadnamen der Dateien. (Natürlich gibt es noch viele andere Möglichkeiten in PS, über Listen zu iterieren.)

++ Ständig benötigte cmdlets, die keine weiteren Abhängigkeiten zu anderen cmdlets haben und die selber ggfs lange laufen oder ressourcenhungrig sind, in binären cmdlets implementieren. Jede NET-Sprache geht, man muß nur System.Management.Automation referenzieren und von Cmdlet oder PsCmdlet ableiten. HowTos gibt es dafür genügend.

++ Sich kundig machen in bezug auf "Lazy Loading", LINQ und was das yield-Schlüsselwort tut. Das funktioniert leider in PS nicht, dazu muß man ein binäres cmdlet bauen. Vorteil ist, daß man auf diesem Weg den Weg der Daten nur beschreiben muß und sie insbesondere NICHT fünfmal laden und siebenmal iterieren muß. Stattdessen werden auf diesem Weg Bindungen erst zur Laufzeit aufgelöst und nur exakt die Daten angefaßt, die benötigt werden, WENN sie benötigt werden. Entsprechend läßt sich eine Liste von 1 Mio oder mehr Elementen in einem einzigen Schritt anlegen - sie ist dann leer, beinhaltet aber ein sogenanntes Versprechen (cf. Promises) Daten zu liefern, wenn man sie haben will.


++ und das wichtigste, vernünftig an Probleme herangehen. Bubblesort ist nicht vernünftig und fünf Schleifen verschachteln auch nicht. Summenformeln mit Schleifen implementieren ist entsprechend Blödsinn, wenn es eine geschlossene Formel dafür gibt. Kurz, in den allermeisten Fällen ist die einfachere Option auch die bessere, aber zugegebenermaßen nicht immer die intuitive.

++ Daher immer erstmal überlegen. Was will ich überhaupt? Vielleicht geht das Problem auch in fünf statt in fünftausend Codezeilen.
 
M

Micha45

Gast
Ich habe diesen Thread erst gestern entdeckt.
Feine Sache und hochinteressant, muss ich sagen und ich hoffe, dass das Thema weitergeführt werden wird.

Es ist zum Teil schon erstaunlich, welche Unterschiede es da in Sachen Performance gibt.
Bei meinen relativ kleinen Projekten fällt mir der Unterschied jetzt nicht so gravierend auf, aber bei sehr komplexen Sachen könnte ich mir schon vorstellen, dass die Unterschiede merklich wahrnehmbar sind.

Hier ist noch eine Webseite, auf der noch weiterführende Tests dargestellt werden:
https://www.norlunn.net/2019/10/17/powershell-performance-tips/
 

RalphS

Lt. Commander
Dabei seit
Feb. 2015
Beiträge
1.735
Damit’s nicht in Vergessenheit gerät:

- Statt [void] zum Verwerfen von Rückgabewerten in PS (und nur da) sollte man zu $null zuweisen.
Das ist zugegeben ein sehr seltsames Konstrukt, ist aber in PS die bevorzugte und performantere Option.
- Ausgaben sind immer teure Vorgänge. In Schleifen sollte man sie so möglich vermeiden. Ausnahme: man arbeitet mit Pipelines.
 
M

Micha45

Gast
Nur mal so als Anregung gedacht und bitte nicht als Kritik auffassen:

ich möchte den Leuten, die mit PowerShell zu tun haben und PowerShell-Anfänger sind, hier mal eine Sammlung meiner bisher gesammelten Erfahrungen bezüglich Performance in PowerShell mittteilen.]

Die Idee ist sicher durchaus lobenswert. Aber ihr solltet hier bitte mal euer "Fachchinesisch" zurückfahren und euch in euren Ausführungen, gerade in Bezug auf Anfänger oder Neueinsteiger, auf eine andere Ebene begeben.

Der Guide ist extra im (hoffentlich) leichten Niveau gehalten, damit auch Anfänger damit etwas anfangen können.
Nein, leider nicht.

Anfänger und Neueinsteiger blicken hier bereits nach ein paar wenigen Zeilen nicht mehr durch und werden mit Fachausdrücken förmlich erschlagen. Das motiviert sicher die Wenigsten, hier am Ball zu bleiben.
Für mich als "fortgeschrittener Anfänger" ist es bereits schon relativ schwierig und noch bin ich einigermaßen in der Lage, den Ausführungen zu folgen und das Meiste nachzuvollziehen. Bis jetzt!

Und den Profis erzählt ihr hier nix Neues.

Statt [void] zum Verwerfen von Rückgabewerten in PS (und nur da) sollte man zu $null zuweisen.

Warum nicht mit einem Beispiel, wie das aussehen könnte?
PowerShell:
# Statt ...
[Void]$WinForm.ShowDialog()

# So ...?
$WinForm.ShowDialog() > $Null
 

RalphS

Lt. Commander
Dabei seit
Feb. 2015
Beiträge
1.735
Danke für den Einwand. :daumen:

Ich würde ja Besserung geloben, aber ich kenn mich und halte mich dann eh nicht dran, deswegen im Zweifel lieber nachhaken. :)

Das "warum nicht mit einem Beispiel" ist schnell erklärt, a war ich unterwegs und hatte nur eine miese Smartphonetastatur und b hatte ich "zuweisen" geschrieben und angenommen, daß jeder weiß, was gemeint ist.

Letztlich führen eh verschiedene Wege zum Ziel. PS hat aber die Angewohnheit, implizit ohne jede Aufforderung in die Pipeline zu schreiben und wenn man eine Ausgabe nicht selbst verwertet, dann kriegt man mit Pech die falschen Informationen.
Zum schnellen Nachstellen:

PowerShell:
# Erstelle eine ArrayList zur Veranschaulichung (da sieht man das Problem schön)
# PS5:
PS> [System.Collections.ArrayList] $testlist =[System.Collections.ArrayList]::new()
# Bis PS4: $testlist = new-object System.Collections.ArrayList

# die neue Liste ist leer. Echo ist implizit => einfach hinschreiben
PS> $testlist
# wir fügen etwas hinzu. Note:  Arraylist kennt keine Elementtypen, das sind alles unspezifizierbare "Objekte".
PS> $testlist.Add('neues Element')
0
# Wo kommt die 0 her?
PS> $testlist.Add('neues Element 1')
1
# Wie erwähnt: Echo ist implizit, jede Codezeile hat deshalb ein "unsichtbares Echo" vorne dran
PS> $testlist.Add('neues Element 2')
2
# ... und Arraylist::Add hat einen Rückgabewert, den Index des neuen Elements innerhalb der Liste.

# Die Liste ist nun etwas mehr gefüllt. Note: die Zahlen von oben sind nicht Teil des Inhalts.
PS> $testlist
neues Element
neues Element 1
neues Element 2

# Wir wollen die Zahlen aber gar nicht haben. Deswegen müssen wir sie gesondert behandeln.

# Lustige Syntax-Option 1: Rückgabewert zum Typ void deklarieren. Damit verfällt er.
PS> [void] $testlist.Add('neues Element A1')
#und weg ist die 3. Warum 3? Weil bereits Indices 0, 1 und 2 belegt wurden und der nächste freie gewählt wird.

# Lustige Syntax-Option 2: Zuweisung zu $null.
PS> $null =  $testlist.Add('neues Element A2')
# auch die 4 sehen wir nun nicht mehr. Dies ist die bevorzugte und performantere Option.
# Hinweis: Zuweisung zu NULL funktioniert in den meisten anderen Sprachen nicht. Das ist eine Besonderheit von Powershell.

# Ganz wichtig:  .Add() hat trotzdem gemacht, was es sollte. Nur der Rückgabewert wurde verworfen.

PS> $testlist
neues Element
neues Element 1
neues Element 2
neues Element A1
neues Element A2

# Protip: PowerShell (ab zumindest 5, früher ggfs auch) verrät Signaturen,
# wenn man den Namen der Funktion angibt - nur den Namen, KEINE Klammern und auch keinen Inhalt dazu.

PS> $testlist.Add
OverloadDefinitions
-------------------
int Add(System.Object value)
int IList.Add(System.Object value)

<#
So sieht man sofort: die Methode .Add() eines Objekts vom Typ Arraylist möchte ein beliebiges Objekt haben (noch allgemeiner als System.Object gibt es nicht) und gibt einen Wert vom Typ int zurück, also eine Zahl.
(Das zweite IList.Add() kann man an der Stelle ignorieren, das ist formal eine zweite Methode Add, die sich aber nicht von der ersten unterscheidet.)
#>

Für den Anfänger ist das natürlich einiges weit oben angesiedelt und auch nur bedingt wichtig, exakt auf welchem Weg man den Rückgabewert nun los wird.

Wichtig ist aber durchaus, daß man ihn A loswerden kann und B ggfs. auch loswerden muß.

Eventuell interessant zu wissen:
unter C# gibt es diese Möglichkeit auch, Rückgabewerte explizit loszuwerden. Normal verfallen die ohnehin, es macht also keinen sichtbaren Unterschied, aber wenn man zur "Pseudovariable" _ (Unterstrich) zuweist, also sowas wie
C#:
_ = testList.Add(5);
sagt, dann ist as auch ein µ performanter, als wenn man es weglassen würde.
Auch unter C# ist das die empfohlene Herangehensweise.
 
Zuletzt bearbeitet:
Top