PS Performance
Powershell ist das dafür berühmt, mit einem "Einzeiler" große Aufgaben zu lösen. Aber mit Powershell können auch ganz umfangreiche Programme entwickelt werden. Wenn Umgebungen aber größer werden, dann ist Performance mehr und mehr ein Thema. Es gibt je nach eingesetztem Befehl durchaus mächtige Unterschiede.
Measure-Command
Erster Schritt ist natürlich das "Messen" der Laufzeit. Dazu kann das Commandlet "Measure-Command" dienen, welches die Laufzeit eines Anweisungsblocks ermittelt. Allerdings führt das Commandlet den Block so erst mal nur "einmal" aus und zudem werden die Ausgaben des Codes an die Pipeline unterdrückt, was wieder Verfälschungen mit sich bringen kann. Für einfache Aufgaben ist es aber ausreichbar.
PS C:\> Measure-Command {Get-ChildItem}
Days : 0
Hours : 0
Minutes : 0
Seconds : 0
Milliseconds : 4
Ticks : 41882
TotalDays : 4,8474537037037E-08
TotalHours : 1,16338888888889E-06
TotalMinutes : 6,98033333333333E-05
TotalSeconds : 0,0041882
TotalMilliseconds : 4,1882
Der Befehlsblock in geschweiften Klammern kann auch durchaus mehrere Codezeilen enthalten. Allerdings ist der Code gekapselt, d.h. Ausgaben, die hier bei "Get-Childitem" natürlich vorhanden sind, gehen unter. Variable-Zuweisungen in dem Block sind aber auch außerhalb des Blocks verwendbar. Das folgende geht also schon
$laufzeit = measure-command {
$recipientlist = get-recipient}
write-host "Laufzeit $laufzeit"
forach ($recipient in $recipientlist) {
write-host "Recipient:"$recipient.identity
}
Wer damit misst, sollte die Befehle immer mehrfach aufrufen, denn die Laufzeit schwankt. Oft dauert gerade der erste Start länger und Folgeaufrufe sind zügiger. Sobald Abhängigkeiten mit anderen Diensten, z.B. LDAP-Abfragen, Webservices, im Spiel sind, müssen die Ergebnisse relativiert betrachtet werden.
Gerade auch kurze Befehle sollten über eine Schleife einfach mehrfach gestartet werden, z.B.
measure-command {
1..100000 | %{
get-date "01.01.1900"
}
}
Wer mehr oder mehrere Punkte im Code messen möchte, kann dies mit GET-DATE tun, um die aktuelle Zeit sehr genau zu erhalten und von vorher ermittelten Werten abzuziehen.
- Measure-Command
http://technet.microsoft.com/de-de/library/dd347702 - Using the Measure-Command
Cmdlet
http://technet.microsoft.com/en-us/library/ee176899.aspx - Measuring Elapsed Time in
Powershell
http://poshtips.com/2010/03/30/measuring-elapsed-time-in-powershell/
Beispiele
Um die Auswirkungen anschaulich zu machen, habe ich drei Aufgaben heran gezogen, die das Potential aufzeigen sollen:
- Exchange Empfänger ermitteln
Das ist sicher eine häufige Aufgabenstellung eine Liste von Empfängern nach Kriterien zu erstellen - PSCustomObjekt als Ausgabe
vorbereiten
Oft erforderlich, wenn Sie als Rückgabe eine Liste mit Objekten erzeugen wollen, die wiederum Properties nutzt. - Variable Zuweisen
Am Ende noch ein ganz einfaches Beispiel für die Initialisierung von Variablen. - Das "Count" Property
Warum die einfache Anzeige der gefundenen Objekte Zeit kostet - Debugausgaben mit Write-Host
Wichtige Informationen bremsen extrem
Schauen wir uns die Ergebnisse im einzelnen an:
Beispiel 1: Exchange Empfängerabfrage / AD Abfrage
Ich denke für Exchange Administratoren ist die Abfrage der Exchange Empfänger ein häufiger Prozess, der auf mehrere Wege durchgeführt werden kann.
- Get-Recipient
Dieses Exchange Commandlet nutzt die Exchange Remote Powershell um Exchange Empfänger zu ermitteln. Es ist sehr leistungsfähig und liefert nett bereitgestellte Objekte als Rückgabe. - Get-ADObject
Etwas einfacher ist Get-ADObject aus dem Windows 2008 Powershell Modul "ActiveDirectory". Es macht aber auch keine direkten LDAP-Abfragen, sondern nutzt die ADWS-Dienste eines Windows 2008 DCs oder höher. - [ADSI]
Der gute alte ADSI-Zugriff von Powershell geht direkt per LDAP an die Server. Das Ergebnis sind "Rohdaten" ohne weitere Unterstützung, durch den LDAP-Zugriff ist dieser Weg nicht nutzbar, wenn Office 365 im Spiel ist und eine Verarbeitung über die Pipeline ist uneffektiv, da ein "Findall()" immer erst alle Daten sucht ehe es weiter geht. Dafür funktioniert dies auch ohne Exchange Module auf dem PC - LDIFDE/CSVDE
Außer Konkurrenz laufen diese beide Programme, die ich in DirSync-Projekten oft verwende. Anscheinend haben die Entwickler dieser Programme ziemlich viel optimiert, denn ein Export per CSVDE ist fast immer noch um einiges schneller als alle der drei vorherigen Optionen. Ich denke das sind dann die Einschränkungen eines interpretierten Skripts
Ich habe alle drei Optionen einmal gegeneinander gestellt, um die Performance in einer etwas größeren Umgebung zu demonstrieren. Das Beispiel fragt ca. 30.000 Einträge ab. Um die Ausgabe der Objekte zu ersparen, wird einfach nur die Anzahl der Objekte zurück gegeben. Dennoch sind die Zeiten natürlich nur eine Momentaufnahme meiner Umgebung. Aber es ist offensichtlich, das "höherwertigen" Commandlets und speziell die Exchange remote Powershell deutlich langsamer. Insofern muss man speziell für aufwändige Auswertungen mit vielen Empfängern überlegen, ob bei der Suche schon gefiltert oder ein schnellerer Weg genutzt wird.
Die Dauer gibt eine Momentaufnahme meiner VMs wieder und ist nicht repräsentativ. Es gibt Varianten bei denen die Initialisierung länger dauert aber beim Zugriff dann schneller sind während andere sofort aber dann nicht so schnell Daten liefern. Auch kann über die Pagesize ein Tuning erfolgen.
| Option und Code | Laufzeit |
|---|---|
|
Exchange Powershell-Befehl measure-command { `
write-host "total:"(get-recipient -resultsize unlimited).count
}
|
349 Sek |
|
Abfrage per Windows 2008 Get-ADObject, Erfordert aber einen Windows 2008 DC mit ADWS measure-command { `
write-host "total:"(get-adobject `
-ldapfilter "(mailnickname=*)" `
-ResultSetSize $null `
-server "server:3268").count
}
|
733 Sek |
|
Klassische Abfrage per ADSI. measure-command {
$root = [system.directoryservices.activedirectory.forest]::getcurrentforest().rootdomain.name
$adsearch= New-Object System.DirectoryServices.DirectorySearcher([ADSI]"GC://$root")
$adsearch.Propertiestoload.add("distinguishedname") | out-null
$adsearch.Propertiestoload.add("mail") | out-null
$adsearch.Propertiestoload.add("ProxyAddresses") | out-null
$adsearch.pagesize = 100
$adsearch.filter = "(mailnickname=*)"
$adsearch.filter = "(objectclass=*)"
$adresult =$adsearch.findall()
write-host $adresult.count
}
# Ausgabe dann per FOR-Schleife
ForEach ($strResult In $adresult) {
$strDN = $strResult.Properties.Item("distinguishedName")
Write-Host "DN:"$strDN
}
|
30 Sek |
|
Klassische Abfrage per ADO (COM-Objekt) measure-command {
$adoConnection = New-Object -comObject "ADODB.Connection"
$adoConnection.Open("Provider=ADsDSOObject;")
$adoCommand = New-Object -comObject "ADODB.Command"
$adoCommand.ActiveConnection = $adoConnection
$adoCommand.Properties.Item("Page Size") = 100
$adoCommand.Properties.Item("Timeout") = 30
$adoCommand.Properties.Item("Cache Results") = $False
$root = [system.directoryservices.activedirectory.forest]::getcurrentforest().rootdomain.name
$strbase=
|
26 Sek |
|
Außer Konkurrenz habe ich die Liste einfach mal mit CSVDE in eine Datei exportiert. measure-command {
CSVDE -f Benutzer.csv -r "(mailnickname=*)" -l "mail,proxyaddresses" -s msxfaq.net -t 3268
}
Obwohl die Daten also noch in eine Datei geschrieben werden, ist CSVDE sehr schnell unterwegs. |
7 Sek |
Allerdings muss man hier natürlich zugeben, dass die Exchange Powershell für den Get-Recipient einige Fehler mehr aus dem AD auslesen muss, um das Ausgabeobjekte zu befüllen. Beim ADSI-Query kann ich genau angeben, welche Felder ich benötige und damit natürlich Zeit sparen.
Beispiel 2: Variablen zuweisen
Diese Aufgabe ist eine kleine Vorarbeit für Beispiel 3. Stellen Sie sic vor ich initialisiere eine Datenstruktur und habe mehrere Felder um ein Datum zu speichern. Um später zu erkennen, welche Felder gar nicht beschrieben wurden, wird ein Startdatum weit in der Vergangenheit gesetzt. So funktionieren "Vergleiche" weiterhin. Stellen sie sich nun vor, Sie müssen eine umfangreiche Liste mit vielen Objekten aufbauen und alle Datumfelder so setzen. Dann ist das schon ein Zeitfaktor. Also starten wir mit der Generierung eines Datums, welches drei Variablen füllt:
| Option und Code | Laufzeit |
|---|---|
|
Klassischer Weg über Get-Date Commandlet
measure-command
{ |
5,7 Sek |
|
Alternativer Weg mit Konvertierung
measure-command
{ |
1,8 Sek |
|
Es geht sogar noch schneller. Da ein DATETIME kein komplexes Objekt, sondern ein Basistyp ist, ist eine Zuweisung keine Referenz, sondern eine Kopie (Clone)
measure-command
{ |
1,2 Sek |
Es kann also auch in Powershell sinnvoll sein, eine Start-Wert, der häufiger verwendet wird, schon vorzubereiten.
Beispiel 3: Objekte für Listenaufbau belegen
Viele meiner Skipte erstellen Berichte und Ausgaben, die im Code schon als CSV-Datei exportiert werden sollen. Diese Ergebnisse eines Datensatzes speichert man natürlich nicht in vielen einzelnen Variablen, sondern fasst diese als "PSCustomObjekt" zusammen. Bei der Initialisierung muss man natürlich darauf achten, dass jedes Ergebnis wieder mit einem neu initialisierten Objekt anfängt. Ein "$obj1=$obj2" ist bei PSCustomObjekts schlecht, da dabei nicht das Objekt kopiert wird, sondern nur eine Referenz erstellt wird. Ändert man dann $obj1, dann ändert sich auch $obj2. Folgendes Beispiel verdeutlicht das.
Das wirkt sogar noch nach, wenn das Objekt genutzt wird, um es in ein Dictionary oder eine Arrayliste zu addieren. Also muss ich das Objekt neu aufbauen, oder einen "Clone" erstellen. Leider kann man PSCustomObjects nicht einfach Clonen, so dass ich es einfach neu aufbaue. Und dazu gibt es drei Optionen.
| Option und Code | Laufzeit |
|---|---|
|
Option1: Klassischer Weg ein PSCustomObjekt zu instanzieren und mit Werten zu beladen measure-command {
1..10000 | %{
$newpso =new-object psObject
$newpso | Add-Member -MemberType noteproperty -Name "mailaddress" -Value "notset"
$newpso | Add-Member -MemberType noteproperty -Name "PrimarySmtpAddress" -Value "notset"
$newpso | Add-Member -MemberType noteproperty -Name "TotalItem" -Value 0
$newpso | Add-Member -MemberType noteproperty -Name "date1" -Value (get-date 01.01.1900)
$newpso | Add-Member -MemberType noteproperty -Name "date2" -Value (get-date 01.01.1900)
$newpso | Add-Member -MemberType noteproperty -Name "date3" -Value (get-date 01.01.1900)
}
}
|
24 Sek |
|
Option2: Man kann aber auch bei der Instanzierung des Objekts schon Properties mitgeben und füllen, das schon deutlich schneller geht. Measure-Command {
1..10000 | %{
$newpso = New-Object PSObject -Property @{
mailaddress = "NotSet"
PrimarySmtpAddress = "NotSet"
totalItem = 0
date1 = (Get-Date 01.01.1900)
date2 = (Get-Date 01.01.1900)
date3 = (Get-Date 01.01.1900)
}
}
}
|
7 Sek |
|
Option3: Alternativer Weg ein PSCustomObjekt zu instanzieren und mit Werten zu beladen measure-command {
1..10000 | %{
$newpso = ("" | select mailaddress,PrimarySmtpAddress,TotalItem,Date1,date2,date3)
$newpso.mailaddress="NotSet"
$newpso.PrimarySmtpAddress="NotSet"
$newpso.totalItem=0
$newpso.date1=(get-date 01.01.1900)
$newpso.date2=(get-date 01.01.1900)
$newpso.date3=(get-date 01.01.1900)
}
}
|
8 Sek |
|
Option4: Zuletzt noch ein Beispiel eines Lesers mit C# als Basisklasse. (Tipp kam von Claudio Spizzi) Add-Type @"
namespace Test {
public class MyObject {
public string mailaddress;
public string PrimarySmtpAddress;
public int totalItem;
public System.DateTime date1;
public System.DateTime date2;
public System.DateTime date3;
}
}
"@
(Measure-Command {
1..10000 | %{
$newpso5 = New-Object Test.MyObject
$newpso5.mailaddress="NotSet"
$newpso5.PrimarySmtpAddress="NotSet"
$newpso5.totalItem=0
$newpso5.date1=(Get-Date 01.01.1900)
$newpso5.date2=(Get-Date 01.01.1900)
$newpso5.date3=(Get-Date 01.01.1900)
}
}).TotalSeconds
Diese Version ist noch mal ein klein wenig schneller als die Option3. |
7 Sek |
|
Option5: Alternativ könnte man natürlich ein Masterobjekt ablegen, welche einfach kopiert wird.
$newpso = ("" |
select
mailaddress,PrimarySmtpAddress,TotalItem,Date1,date2,date3) Dieser Wert ist natürlich optimistisch. Wenn Sie die Defaultwerte ändern wollten, dann muss nach dem Copy natürlich noch Zeit für die Zuweisung eingeplant werden. |
3 Sek |
Es ist gut zu sehen, dass der Trick mit dem "select" deutlich schneller ist als der klassische Ansatz oder der Versuch ein Objekt zu kopieren. Beim klassischen Ansatz kostet es einfach zu viel Zeit immer wieder "Add-Member" aufzurufen.
Beispiel 4: Das "Count-Property"
Es ist so schön einfach, wenn ein Skript per ADO eine Datenbankabfrage oder per ADSI eine Suche im Active Directory durchgeführt hat und voller Stoltz ist das erste, was ein Programmierer als nächste Zeile einbaut ein
write-host "Gefundene Elemente:" $resultset.count
Unschön dabei ist, dass sowohl ADO als auch ADSI erst dann einen "Count" zurück geben können, wenn Sie die ganzen Daten einmal ausgelesen haben. Sie verlieren also kostbare Zeit. Wenn die Anzahl der Ergebnisse im Skript nicht für etwas anderes erforderlich ist, z.B. eine Fortschrittsanzeige auszugeben oder die Gültigkeit der Rückgabe zu prüfen, dann kann man das einfach sein lassen und direkt zur Verarbeitung der Ergebnisse übergehen.
Es spricht ja nichts dagegen bei der Verarbeitung einige nützlichere Counter mit hochzuzählen und auszugeben.
Beispiel 5: "Write-host" und Start-Transcript
Zugegeben ich schreibe viele Skripte und natürlich möchte ich wissen, was die Skripte genau tun. Ich kann ja nicht jedes Skript permanent im Debugger laufen lassen. Also machte ich, was auch viele andere Administratoren, Consultants und Entwickler machen: Sie schreiben "Debugausgaben" in das Script. Und nichts ist einfacher als ein "Write-Host" und wenn ich das auch mit protokollieren will, dann addiere ich noch ein Start-Transcript am Anfang.
Aber genau die Ausgaben auf dem Bildschirm machen die Skripte langsam, da die Ausgabe auf dem Bildschirm gescrollt werden muss etc. Nicht umsonst habe ich mir eine eigene Routine gebaut, die "Write-Host" ersetzt und die Ausgaben direkt in eine Datei umlenkt und einige Mehrwertfunktionen bietet. (Siehe MSXFAQLogger).
Kleiner Tipp: Wenn Sie viele Daten verarbeiten und einen Statusupdate erhalten wollen ohne dass der Bildschirm allzu viel scrollt, dann zeigen Sie doch den Fortschritt mit einer nummer auf, die nur all 100 oder 1000 Änderungen aktualisiert wird.
if (!($count%1000)) {write-host "`rUsers:"$count -nonewline}
Write-Host ist also nicht das schnellste Modul, aber geht es schneller ?. Ja, z.B. über [console]
| Option und Code | Laufzeit |
|---|---|
|
Option1: Ausgabe von 1000 "Textzeilen" mit write-host. measure-command {
1..100000 | %{
write-host "hallo"
}
}
|
1,1 Sec/1000 101 Sek/100000 |
|
Option2: Ausgabe von 1000 "Textzeilen" mit [console] measure-command {
1..100000 | %{
[console]::Write("hallo`n")
}
}
|
0,4 Sec/1000 36 Sek/100000 |
|
Option3: Ausgabe von 1000 "Textzeilen" mit "out-host". Natürlich nur nutzbar, wenn Sie STDOUT als Pipelineausgabe nicht anderweitig benötigen. Zudem schummelt das Script bei Measure-command, da keine Ausgabe erzeugt wird $start=get-date
1..100000 | %{
"hallo"
}
write-host "dauer" ((get-date)-$start).totalseconds
|
2 Sek/1000 66 Sek/100000 |
|
Spart man sich aber die Ausgabe auf den Bildschirm und lässt die Daten in eine Datei schreiben, dann wird es noch langsamer, da das Anhängen an die Datei scheinbar viel Zeit braucht. measure-command {
1..1000 | %{
if (!($_ % 100)) {write-host $_}
"hallo" | out-file .\debug.txt -append
}
}
|
11 Sec/1000 |
|
Dann doch besser die Ausgaben erst mal im Speicher aufaddieren und am Ende schreiben, was aber das Risiko hat, dass beim Abbruch des Skript gar nicht geschrieben wurde. Es sei denn sie fangen das mit einem TRAP ab um dann noch mal zu schreiben. Und es ist gut zu sehe, dass die Verarbeitung von Zeile zu Zeile langsamer wird, egal ob $result nun ein String oder Array. measure-command {
Trap {"Error: $_"; $result | out-file .\debug.txt ;Break;}
$result=@() # oder [string]$result=""
1..100000 | %{
if (!($_ % 100)) {write-host $_}
$result+="hallo"
}
$result | out-file .\debug.txt
}
|
0,1Sek/1000 137Sek/100000 |
|
Dass speziell das Schreiben in Dateien noch schneller gehen kann beweist wieder mal, dass eine direkte Nutzung der .NET-Klassen einen großen Vorteil haben kann. Der Streamwriter/TextWriter nutzt intern einen Buffer um nicht jede Ausgabe sofort zu schreiben, sondern in Blöcken. measure-command {
$debugfile = New-Object System.IO.StreamWriter "c:\temp\debug.txt"
1..100000 | %{
if (!($_ % 100)) {write-host $_}
$debugfile.WriteLine("hallo")
}
$debugfile.Close();
}
Analog könnte man noch einen "System.IO.StringWriter" nutzen, der aber gleich schnell ist. Allerdings sollten Sie ein Skript dann nicht mit "CTRL-C" abbrechen, da dann die Handle nicht geschlossen werden. Erst ein Beenden der Powershell gibt die Handles wieder frei. |
0,1Sek/1000 10Sek/100000 |
Der StreamWriter ist also bezüglich "schreiben" durch nichts zu übertreffen und damit die erste Wahl, um Debugfiles wegzuschreiben. Sie sehen schon die Laufzeitunterschiede einer einfachen Ausgabe.
- StreamWriter-Klasse
http://msdn.microsoft.com/de-de/library/system.io.streamwriter(v=vs.80).aspx - Add-Content and Out-File are not for
performance
http://sqlblog.com/blogs/linchi_shea/archive/2010/01/04/add-content-and-out-file-are-not-for-performance.aspx
Beispiel 6: Listen erweitern
Natürlich lebt Powershell auch von der Pipeline. Aber manchmal möchte ich die Ergebnisse doch erst in einer Liste sammeln und danach weiter verarbeiten oder z.B. mit Export-CSV innerhalb des Programms in eine Datei exportieren. Also nehme ich die Werte und "addiere" diese. Am Beispiel mit String könnte das so aussehen:
[String]$ergebnis = ""
foreach ($repeat in 0..9) {
$start = get-date
foreach ($zeile in 0..999) {
$ergebnis += "Zeile $repeat $zeile"
}
write-host "Repeat $repeat Dauer: "((get-date) - $start).totalseconds
}
Ausgabe:
Repeat:0 Dauer: 0,0210012
Repeat:1 Dauer: 0,0150009
Repeat:2 Dauer: 0,0220013
Repeat:3 Dauer: 0,053003
Repeat:4 Dauer: 0,297017
Repeat:5 Dauer: 0,3610207
Repeat:6 Dauer: 0,5040289
Repeat:7 Dauer: 0,5900338
Repeat:8 Dauer: 0,6060346
Repeat:9 Dauer: 0,6610378
Ich habe noch nicht hinter die Kulissen geschaut aber anscheinend kopiert Powershell (oder .NET) intern die Variable beim Erweitern um statt dynamische mit Pointern zu arbeiten. Es ist also vielleicht nicht die beste Idee, so eine Liste aufzubauen. Das gilt insbesondere, wenn sie ganz große Strukturen über Strings aufbauen wollen.
Deutlich besser schlagen sich Hashtabellen:
[hashtable]$ergebnis = @{}
foreach ($repeat in 0..9) {
$start = get-date
foreach ($zeile in 0..999) {
$ergebnis.add( ([string]$repeat+[string]$zeile),"Zeile $repeat $zeile")
}
write-host "Repeat:$repeat Dauer: "((get-date) - $start).totalseconds
}
Repeat:0 Dauer: 0,0370021
Repeat:1 Dauer: 0,0130008
Repeat:2 Dauer: 0,0100006
Repeat:3 Dauer: 0,0100005
Repeat:4 Dauer: 0,0150008
Repeat:5 Dauer: 0,0090005
Repeat:6 Dauer: 0,0090005
Repeat:7 Dauer: 0,0130007
Repeat:8 Dauer: 0,0100006
Repeat:9 Dauer: 0,0120007
Im Gegensatz dazu schlagen sich Arrays sogar noch schlechter:
[Array]$ergebnis = @()
foreach ($repeat in 0..9) {
$start = get-date
foreach ($zeile in 0..999) {
$ergebnis += "Zeile $repeat $zeile"
}
write-host "Repeat:$repeat Dauer: "((get-date) - $start).totalseconds
}
Repeat:0 Dauer: 0,0740042
Repeat:1 Dauer: 0,1830105
Repeat:2 Dauer: 0,2580147
Repeat:3 Dauer: 0,3360192
Repeat:4 Dauer: 0,4260243
Repeat:5 Dauer: 0,52503
Repeat:6 Dauer: 0,6060346
Repeat:7 Dauer: 0,70004
Repeat:8 Dauer: 0,8160467
Repeat:9 Dauer: 1,1250643
Weitere Variablen habe ich aber nicht untersucht
Haben Sie eine bessere Idee für ein Aufbau einfacher Listen ?
Beispiel 7: Funktionen einbinden
Aus Gründen der Modularisierung und Wiederverwendung von Code bietet es sich an, geeignete Codeabschnitte als Funktion in eigene PS1-Dateien oder PS1M-Module auszulagern und im Hauptskript dann einzubinden. Auch dazu gibt es unterschiedliche Ansätze, die sich auf die Performance niederschlagen können.
Achtung:
Die Beispiele hier zeigen rein den Overhead der
jeweiligen Einbindung. Da der Code aber nicht
"aktiv tut", sind die Zahlen viel drastischer
aber dennoch nicht zu unterschätzen.
Das Muster besteht aus einem MAIN-Skript, welches in zwei Fällen auf eine ausgelagerte Funktion in einem SUP.PS1 zurück greift.
| Version | Main.ps1 | Sub.ps1 | Laufzeit | Bemerkung |
|---|---|---|---|---|
| Einfache Schleife |
$sum=0
foreach ($item in 1..1000) {
$sum += $item
}write-host "Main End sum: $sum"
|
Entfällt | 9msec | Nicht modular |
| Unterroutine in der Schleife |
$sum=0
foreach ($item in 1..1000) {
$sum += . .\sub.ps1 #
}write-host "Main End sum: $sum"
|
$item |
1680 msec | Die Unterroutine wird immer wieder aufgerufen, was letztlich sehr langsam ist. |
| Einbindung als Funktion |
function add-item {
$item
}
$sum=0
foreach ($item in 1..1000) {
$sum += add-item
}write-host "Main End sum: $sum"
|
Entfällt | 120msec | Die Auslagerung in eine Funktion kostet schon Performance im Vergleich zu den 9 msec der direkten Nutzung. Modular ist das aber nicht |
| Einbindung als externe Funktion über DOT Sourcing |
. .\sub.ps1 # einbinden
$sum=0
foreach ($item in 1..1000) {
$sum += add-item
}
write-host "Main End sum: $sum"
|
#sub.ps1
function add-item {
$item
}
|
120 msec | Lagert man die Funktion au saber bindet sie einmal ein, dann ist es vergleichbar schnell. So kann Modularität funktionieren. |
Interessant ist schon, dass allein die Auslagerung in eine Funktion innerhalb der gleichen PS1-Datei schon die Ausführung derart verlangsamt. Wird aber die Funktion in jeder Schleife neu aus einer anderen PS1-Datei bezogen, dann leidet die Performance stark. Bindet man die Funktion aber vorher über DOT-Sourcing mit ein, dann ist man nicht schlechter dran, als wenn man die Funktion im gleichen Skript definiert.
Beispiel 8: Textdateien lesen
Textdateien sind eigentlich was schönes, da man sie einfach lesen und schreiben kann und in fast jedem Editor auch anzeigen und bearbeiten kann. Zugegeben kommt Notepad auch unter Windows 8 mit großen Dateien immer noch an seine Grenzen, so dass man gut beraten ist, alternative Editoren (z.B. Notepad++, Textpad) zu verwenden oder gleich die Informationen per Skript zu extrahieren. So bin also auch ich immer mal genötigt, Textdateien schnell zu parsen und mittlerweile ist natürlich Powershell mein Mittel der Wahl. Als ich aber einmal ein 50 Megabyte großes Lync Client Tracefile parsen wollte, ist mir die Langsamkeit aufgefallen und das wollte ich nicht ganz wahr haben. Daraufhin habe ich eine kleine Mess-Serie aufgestellt, wie man eine große Textdatei am besten zeilenweise schnell einliest. Powershell eröffnet dabei doch einige Optionen. Alle Befehle verwendeten die gleiche Powershell und ich habe die Befehle immer zweimal gestartet, um etwaige Verfälschungen durch Caching zu eliminieren. Es kann also sein, dass die Zeiten "zu gut" sind, weil Daten noch in einem Cache gelegen haben. Virenscanner war aktiv aber es war mein Notebook. Hyperschnelle Raid-Controller mit eigenen Caches gibt es also nicht. Die Zeiten wurden mit Measure-command gemessen.
| Methode | Zeit Sek | Beschreibung |
|---|---|---|
| COPY <datei> <datei2> | 0,1 | ich habe einfach mal einen COPY gemacht, der die 50 MB gelesen aber auch wieder geschrieben hat. Was Windows da mi Caches tut, ist mir suspekt aber 50 MB von einer 2,5" IDE-Disk in 0,1 Sek zu lesen und zu schreiben ist mir suspekt. |
| type <date> -> <datei2> | 256 | Ein klassische DOS-Type üer Pipeline in eine Datei ist sehr langsam. Das liegt aber wohl eher am "Append"-Schreiben denn am Lesen. |
| type <datei> -> nul | 0,1 | Die Gegenprobe mit einem Ausgeben nach NUL war dann auch deutlich schneller |
| get-content <datei> | out-null | 52 | Get-Content liest die Datei zeilenweise ein und damit kann man sicher kein Performanceblumentopf gewinnen. Aber scheinbar liest das Comandlet wirklich sehr langsam die Dateien ein. |
| select-string datei -pattern * | out-null | 37 | Auch das Commandlet "Select-String" kann direkt Informationen aus Dateien auslesen. Damit es aber alle Zeilen liefert, muss man den Filter auf ".*" setzen. Trotz erforderlicher Auswertung jeder einzelner Zeile mit dem regulären Ausdruck ist Select-String deutlich schneller als Get-Content. |
| import-csv datei -delimiter | out-null | 36 | Auch das "Import-CSV"-Commandlet kann man für den Import einer Textdatei missbrauchen. Allerdings sollte man den Feldtrenner so vorgeben, dass eben nichts getrennt wird. Dann steht im ersten Feld der Pipeline einfach die komplette Zeile |
| System.File.IO ReadLines | 1,5 |
Das Einlesen mit folgendem Code ist sehr schnell
foreach ($line
in [System.IO.File]::ReadLines($pwd.path+"\Lync-UccApi-0.UccApilog"))
{
|
| System.File.IO ReadAllLines | 1,3 |
Mit ReadAllLines geht es sogar noch ein bisschen schneller.
foreach ($line
in [System.IO.File]::ReadAllLines($pwd.path+"\Lync-UccApi-0.UccApilog"))
{
|
Wenn ich mir das so anschaue, dann ist es schon erschreckend, wie langsam der ein oder andere Code ist. Ich kann aber nicht genau verstehen, warum z.B. "Type" so viel schneller sein soll, so es doch auch nur ein "alias" für "Get-Content" ist.
- Optimizing Performance of
Get-Content for Large Files
http://rkeithhill.wordpress.com/2007/06/17/optimizing-performance-of-get-content-for-large-files/
Weitere Themen
Und das waren nun nur ein paar Beispiele. Es gibt noch weitere Themen, die knifflig sein können, z.B.:
- Eventlog
Es macht sehr wohl einen Unterschied, ob sie mit "Get-WinEvent" oder "Get-Eventlog" arbeiten - Dateizugriffe
Wer mit "out-File" oder "out-csv" arbeitet, nutzt den einfachen aber sicher nicht den schnellsten Weg. StreamWriter sind für große Dateien eine ebenso einfache aber viel schnellere Alternative - Filtern
Viele Exchange Commandlets erlauben die Angabe eines Filters. Wenn Sie nur eine Teilmenge benötigen, sollten Sie nicht erst alle Elemente holen und mit einem "WHERE" die Ausgabe filtern, sondern vorher schon einen Filter bei der Abfrage hinterlegen. Leider lassen sich bei Exchange nicht alle Properties mit OPATH filtern. - Suchen und Vergleichen
Gerade beim Abgleich von Daten muss man überlegen, ob man jede Information in der Quelle im Ziel "sucht" oder ob es nicht vielleicht schneller geht, die Daten des Ziels flugs in eine Hashtable einzulesen und dort dann "im Memory" zu suchen. Das ist insbesondere bei AD-Abfragen oft deutlich schneller anstatt den DC mit massenhaft Anfragen zu belästigen.
Insofern kann es sich schon mal lohnen zu schauen, wo ein Code seine Zeit verbringt.
Zusammenfassung
Auch wenn Powershell relative einfach ist und passende Commandlets gerade im Bereich Exchange einen Zugriff auf Objekte deutlich unterstützen, so muss zumindest in größeren Umgebungen geprüft werden, ob die einfachen aber ressourcenintensiven Commandlets die Anforderungen an die Laufzeit erfüllen. Ich merke dies immer in Umgebungen, wo ich mit Powershell Skripte zur Auswertung erstelle. Wenn Sie z.B. per Powershell die Adressbücher von zwei Mailsystemen auslesen, um diese dann zu "vergleichen", dann ist es nicht lustig, wenn eine Auswertung 10 Minuten und länger läuft.
Weitere Links
- Measure-Command
http://technet.microsoft.com/de-de/library/dd347702 - Using the Measure-Command
Cmdlet
http://technet.microsoft.com/en-us/library/ee176899.aspx - Powershell and writing files
(how fast can you write to a
file? )
http://blogs.technet.com/b/gbordier/archive/2009/05/05/powershell-and-writing-files-how-fast-can-you-write-to-a-file.aspx - Measuring Elapsed Time in
Powershell
http://poshtips.com/2010/03/30/measuring-elapsed-time-in-powershell/ - PowerShell: Get-WinEvent vs.
Get-EventLog
http://www.mcbsys.com/techblog/2011/04/powershell-get-winevent-vs-get-eventlog/ - How to Improve the
Performance of a PowerShell
Event Log Query
http://blogs.technet.com/b/heyscriptingguy/archive/2011/03/08/how-to-improve-the-performance-of-a-powershell-event-log-query.aspx - Performance with PowerShell
http://tfl09.blogspot.de/2011/11/performance-with-powershell.html






