Powershell Pipe vs Funktionen

pizza4ever

Lt. Commander
Registriert
Apr. 2009
Beiträge
1.711
Hallo zusammen,

in Powershell habe ich ja zum einen die Möglichkeit Funktionen zu schreiben, als auch das ganze in Module zu packen und an die Pipe zu schicken...

Kann mir jemand erklären wann ich was richtigerweise nehme?

Ist eine Technik i.d.R. schneller als die andere?
 
Also Performance-Unterschiede zwischen Funktionen im Script und nachgeladen PowerShell-Funktionen über Module, hab ich bisher nicht feststellen können und sollte es auch nicht geben.

Jedoch gibt's einen großen Performance-Boost, wenn man in einem bestimmten Use Case eine Funktion mehrere Mal in einer Schleife aufrufen muss/will und dabei die Funktion als PowerShell-Klasse (nicht C# Class) schreibt und nicht als normale Funktion.
Im Vergleich zur PowerShell-Klasse kommt es rüber, als ob die PowerShell-Funktion bei jedem Aufruf komplett neu kompiliert/geladen/etc wird.
Die Verzögerung pro Aufruf der Funktion kumuliert sich hier auf Dauer extrem in die Höhe.
(ich hoffe Microsoft bessert hier mal noch aus)

Binär als C# CMDlet rennt's allerdings am Besten.

Zu dem Performance-Unterschied von PowerShell-Klassen, C#-CMDlets und normalen PowerShell-Funktionen werde ich demnächst noch was in mein angefangenes Topic dazupacken.
https://www.computerbase.de/forum/threads/powershell-performance-tipps.1902034/
Die Schreibweise dort ist extra auf leichtem Niveau geschrieben für Anfänger. Eine Art Guide, den ich angefangen habe, was mir, als ich mit PowerShell angefangen habe, eventuell gut geholfen hätte, um direkt performant zu schreiben.

pizza4ever schrieb:
und an die Pipe zu schicken...
Das Pipe'en sollte man so gut es geht vermeiden, wenn man performante Scripts haben möchte, da das bewiesener Maßen langsamer ist, als das in einer ForEach-Schleife durchlaufen zu lassen.
Mit "ForEach-Schleife" ist hier NICHT die Pipe-Methode "Foreach-Object" (Alias % --> $input | %{$_ -eq 'n/a'}) gemeint, sondern das statement keyword "foreach ($item in $array { ... do something ...)"

Gibt allerdings einige Built-In Funktionen/CMDlets die nur über die Pipe funktionieren, z. B. Sort-Object oder ConvertTo-Csv.

Und generell sollte man es vermeiden jedes Item visuell in der Konsole darstellen zu lassen, wie z. B. via Write-Host oder Write-Progress.
Hier muss es ausreichen, wenn man nur in 10% Schritten einen Zwischenstand zurückgibt.

Zu deiner Frage "Wann was"?:
  • Generell gilt: Es muss zum Verwendungszweck passen und ob man das Durchsetzen eines globalen, modularen und versionierten Einsatzes/Standards sinnvoll umsetzen kann.

  • Ich pack generell erstmal alles in ein Script, insbesondere wenn die Funktionen nicht Script-übergreifend nutzbar seien müssen/sollen.
    Dabei kopiere ich auch meine selbstgeschriebenden Funktionen aus meinen Modulen rein.
    Ist irgendwie leichter verteil- und handle-bar.
    Ab 1.000 Zeilen Code sollte man aber überlegen eventuell anders zu handeln.

    Das ist allerdings nur meine Meinung und ein Ansatz, der meistens zu meinen Use Cases passt.
 
Zuletzt bearbeitet:
Naja, wie so oft... Kommt Es Darauf An (tm). :daumen:

@DPXone hat ja schon ein paar Dinge erwähnt.

Aber dennoch ist die Pipeline nicht inhärent schlecht. Ganz im Gegenteil. Die Pipeline ermöglicht sogenanntes lazy loading/late binding. Heißt: Ausgaben werden gefiltert. Wenn ich vorne 1'000 Elemente reinstecke und nach dem nächsten Befehl in der Pipeline nur noch 50 Elemente übrigbleiben, dann werden die nachfolgenden Befehle nicht mehr 1'000fach, sondern (maximal) 50fach ausgeführt.

Als nächstes muß man bei PowerShell im Sinne der Performance immer im Hinterkopf behalten, daß hier seit Version 3 mit Listen gearbeitet wird (unter Win7 heißt das: WMF 5.1 nachinstallieren). Schleifen sind weder erforderlich noch besonders performant.

Beispiel:
PowerShell:
Get-ChildItem -Path abc -File -Recurse| Remove-Item
ist ein typisches PowerShell-Scriptfragment (strukturell).
  • Dateien aus dem Pfad abc werden rekursiv gesucht (nur Dateien, keine Ordner).
  • Sobald eine gefunden wird (!) wird dieser Fund in die Pipeline geschoben.
  • Dort wartet Remove-Item. Ergebnis: noch während der Suche werden die Funde gelöscht. Dateien suchen und löschen ist (pseudo)parallel.

Wie man auch erkennen kann, wird an keiner Stelle berücksichtigt, was Get-Childitem tatsächlich zur Laufzeit findet oder nicht. Eine Datei => funktioniert. Zwei oder mehr => funktioniert. Gar keine Datei => es wird natürlich nichts gelöscht, klar, aber dieser Fall muß ebenfalls nicht berücksichtigt werden.

Man könnte stattdessen auch den Befehl auftrennen:
PowerShell:
[System.IO.FileInfo[]] $files = Get-Childitem -Path abc -File -Recurse

# option A
$files | Remove-Item
# option B
 $files | % {Remove-Item -Path $_.FullName}
foreach($file in $files)
{
# C1
$file | remove-item
# C2
Remove-Item -Path $file.FullName
}

Semantisch passiert hier dasselbe. Man führt den Code aus und hinterher gibt es in abc keine Dateien mehr, wohl aber noch die Unterordner.

Der Unterschied liegt im Detail.
  • zuerst werden alle Dateien gesucht. Das belegt Platz und dauert Zeit.
  • wenn alle Dateien gefunden wurden, werden sie (A) am Stück oder (B, C) per weiterer Schleife gelöscht.
  • Suchen und löschen ist nun seriell.
  • B und C unterscheiden sich dadurch, daß B eine Menge zur Einzelverarbeitung an die Pipeline übergibt und C ein "höherliegendes" Sprachkonstrukt ist. An dieser Stelle ist, wie schon erwähnt, performanter. C1 und C2 sind dagegen völlig austauschbar, auch in bezug auf die Performance, weil hier immer nur ein einziges Dateiobjekt übergeben wird. Formal verarbeitet C1 eine Liste mit einem Element und C2 ein einzelnes Element.

  • Pro, man kann während des Suchens noch abbrechen, ohne daß auch nur eine Datei entfernt wurde.
  • Contra, ich brauch mehr Ressourcen, ich muß mehr Code schreiben und es dauert insgesamt länger.

Man muß also immer schauen, was man genau tut und wie man es angeht. In den meisten Fällen braucht man in PS keine Schleifen - das geht soweit, daß man im Beispiel oben einfach $files.Fullname sagen kann, OBWOHL $files ein Array ist und .Fullname eine Eigenschaft eines einzelnen Objekts -- "normal" geht sowas nicht und soll auch gar nicht gehen.

Wenn man eine Schleife braucht, weil es anders nicht geht: wieder überdenken, was man erreichen möchte. Eine Schleife von..bis... bauen und drin dann jede Menge if then else oder switch($x)? Dann macht man was... nicht falsch, aber suboptimal, weil man mit Pipeline und Where-Object (Alias "?") signifikant performanter und vor allem auch leserlicher dasteht.

Das letzte Bißchen Performance kitzelt man entsprechend aus PS raus, wenn man auf Schleifen ganz verzichtet und überall dort, wo man normalerweise eine verwenden müßte, ein cmdlet schreibt (in einer der NET-Sprachen, sprich kompiliert), welches die Liste der sonst an die Schleife übergebenen Einzelobjekte als Liste annehmen und verarbeiten kann und hinten die fertig verarbeitete Liste gemäß Anforderungen wieder ausspuckt.

Darüber hinaus ist in den meisten Fällen das binäre cmdlet performanter als die Scriptdatei. Andererseits ist das cmdlet natürlich unveränderlich. Entsprechend müßte man an der Stelle ein bißchen planen und natürlich auch fragen, ob es den Extraaufwand wert ist.

Generell: je mehr man zu verarbeiten hat, desto eher sollte man den "Script"-Ansatz zurückstellen und auf binäre Verarbeitung setzen. Dann macht man nur noch ein kleines Script drumrum, was die binär kompilierten Funktionen aufruft.
 
RalphS schrieb:
@DPXone hat ja schon ein paar Dinge erwähnt.

Aber dennoch ist die Pipeline nicht inhärent schlecht. Ganz im Gegenteil. Die Pipeline ermöglicht sogenanntes lazy loading/late binding. Heißt: Ausgaben werden gefiltert. Wenn ich vorne 1'000 Elemente reinstecke und nach dem nächsten Befehl in der Pipeline nur noch 50 Elemente übrigbleiben, dann werden die nachfolgenden Befehle nicht mehr 1'000fach, sondern (maximal) 50fach ausgeführt.

Also als "Lazy Loading" wurde ich das bei PowerShell nicht bezeichnen. ;)
Das läuft einfach meist im stupiden Streaming-Verfahren.

Und das "Streaming" war bei PowerShell bisher noch nie performant bei mir.

Zurück zur Performance:
"ForEach" lädt vorab und zieht alles performant durch.
"ForEach-Object" bzw. Pipe's arbeiten im Streaming-Verfahren. (Zumindest die meisten)
The ForEach statement loads all of the items up front into a collection before processing them one at a time. ForEach-Object expects the items to be streamed via the pipeline, thus lowering the memory requirements, but at the same time, taking a performance hit
Mehr Infos gibts's hier:
https://devblogs.microsoft.com/scripting/getting-to-know-foreach-and-foreach-object/
 
Zuletzt bearbeitet:
Okay, mit Bezeichnungen hatte ich’s noch nie so. 😊

Wie gesagt, für ungefiltertes Jedes X aus Y soll... ist foreach x in y die besser Wahl, aber je mehr nachgefiltert wird (nicht alle x, sondern nur die und die und die) bzw je mehr man linear verketten muß (tu das, dann das, und dann das) desto interessanter wird die Pipeline.

Ist wie mit Extensions und LINQ in C#. Das Codeverständnis steigt bei sinkendem Schreibaufwand, man hat aber im Zweifel einen gewissen Performanceimpact.

Wie man das dann am Ende nennt... meh.
 
RalphS schrieb:
Okay, mit Bezeichnungen hatte ich’s noch nie so. 😊
Wenn du das schon sagst, dann bin ich raus :D
RalphS schrieb:
Wie gesagt, für ungefiltertes Jedes X aus Y soll... ist foreach x in y die besser Wahl, aber je mehr nachgefiltert wird (nicht alle x, sondern nur die und die und die) bzw je mehr man linear verketten muß (tu das, dann das, und dann das) desto interessanter wird die Pipeline.

Ist wie mit Extensions und LINQ in C#. Das Codeverständnis steigt bei sinkendem Schreibaufwand, man hat aber im Zweifel einen gewissen Performanceimpact.

Wie man das dann am Ende nennt... meh.
LINQ ist zwar geil, aber das Linq dann schon irgendwie geil ist, das hab selbst ich als C#- (aber nicht .NET ) Neuling verstanden. :D
Aber bisher fehlt noch der eindeutige, unkomplizierte und performante Einsatz in PowerShell.

Hab aber auch schon mit Linq in PowerShell rumgespielt und werd's auch weiter probieren.
Interessante Ressourcen:
https://www.red-gate.com/simple-talk/dotnet/net-framework/high-performance-powershell-linq/
https://stackoverflow.com/questions/38360545/can-linq-be-used-in-powershell

Aber.... ich werd demnächst noch alles im Topic https://www.computerbase.de/forum/threads/powershell-performance-tipps.1902034/ auseinander nehmen und diskutieren.
 
Zuletzt bearbeitet:
Ich bin grundsätzlich über diesen Artikel gestoßen:

https://www.varonis.com/blog/how-to-use-powershell-objects-and-data-piping/

PowerShell was inspired by many of the great ideas that make up “The Unix Philosophy” – most notable for us today are two points:


  1. Make each program do one thing well. To do a new job, build afresh rather than complicate old programs by adding new “features”.
  2. Expect the output of every program to become the input to another, as yet unknown, program. Don’t clutter output with extraneous information. Avoid stringently columnar or binary input formats. Don’t insist on interactive input.

In practice, what these somewhat abstract points of philosophy mean is that you should create lots of small, purposeful PowerShell scripts that each do a particular task. Every time you go to put an If/Else, another flag, another bit of branching logic, you should ask yourself: “Would this be better as a separate script?”


An example: don’t make a script that downloads a file and then parses the downloaded data. Make two scripts:


  1. One that downloads the data – download.ps
  2. A second that handles parsing the data into something usable – parse.ps

To get the data from the download.ps to parse.ps you would “pipe” the data in between the two scripts.

und ich selbst bau mir nie eigene Pipes sondern bin immer noch im alten "baue eine Funktion mit Übergabewerten" Denken drinnen...
 
Actually.. ich meinte genau den Pipelineansatz in PS als Abbildung des LINQ-Ansatzes in C#.

C#:
IEnumerable<char> lotsofchars = "abcdefghijklmnopqrstuvwxyz"; // oder so
            lotsofchars
            .Where(x => Regex.IsMatch(input: x.ToString(), pattern: "[a-c]"))
            .Select(x => string.Format("Grabbed {0} from input", x));
Dasselbe in PS:
PowerShell:
[string] $lotsOfChars = "abcdefghijklmnopqrstuvwxyz"; # oder so

1 .. $lotsOfChars.Length |
 ? {
 $lotsofchars[$_] -in 'a','b','c'
   } |
% {
'Grabbed {0} from input' -f  $_
}

Es gibt geringfügige Abweichungen, klar, aber der Ansatz ist exakt derselbe: ich kippe meine Menge an Elementen oben rein, ich laß das durch eine Pipeline laufen und am Ende krieg ich mit sehr wenig Aufwand eine transformierte Ausgabe einer Untermenge.
 
Zurück
Oben