Ist die Punktbeschaffung langsamer als nur das Lesen von Dateiinhalten?

13

Ich habe ein PowerShell-Modul geschrieben, das Funktionsdefinitionen aus verschiedenen Quelldateien (dh einer .ps1-Datei pro Funktion) abruft. Dies ermöglicht es uns (als Team), parallel an verschiedenen Funktionen zu arbeiten. Das Modul (.psm1-Datei) ruft die Liste der verfügbaren .ps1-Dateien ab ...

$Functions = Get-ChildItem -Path $FunctionPath *.ps1

... durchläuft dann die Liste und zieht jede Funktionsdefinition per Dot-Sourcing ein:

foreach($Function in $Functions) {
  . $Function.Fullname                                     # Can be slow
}

Problem: Wir haben festgestellt, dass die Geschwindigkeit, mit der dies abgeschlossen wird, sehr unterschiedlich sein kann, von 10 bis 180 Sekunden für etwa 50 Quelldateien, je nachdem, auf welchem ​​Computer wir testen. Wir können die großen Zeitunterschiede nicht erklären und glauben, dass wir Variablen wie Maschinentyp, Betriebssystem, Benutzerkonto, Administratorberechtigungen, PS-Profil, PS-Version usw. gesteuert haben. Die benötigte Zeit kann auf demselben Host für denselben variieren Benutzer von einem Tag zum nächsten.

Wir haben uns gefragt, ob dies ein Problem beim Festplattenzugriff ist, und getestet, wie schnell wir einfach von der Festplatte lesen können. Es stellte sich heraus, dass das Durchlaufen Get-Contentall dieser Dateien sehr schnell war, was wir zur Umgehung des Problems ausgenutzt haben:

foreach($Function in $Functions) {
  Invoke-Expression (Get-Content $Function.Fullname -Raw)  # Is quick
}

Warum ist das Hinzufügen dieser Funktionen per Dot-Sourcing so viel langsamer als das Lesen und Ausführen des Dateiinhalts?

Charlie Joynt
quelle

Antworten:

17

Wissenschaft aufbauen

Zunächst einige Skripte, die uns dabei helfen, dies zu testen. Dadurch werden 2000 Skriptdateien mit jeweils einer kleinen Funktion erstellt:

1..2000 | % { "Function Test$_(`$someArg) { Return `$someArg * $_ }" > "test$_.ps1" }

Das sollte ausreichen, damit der normale Startaufwand keine Rolle spielt. Sie können bei Bedarf weitere hinzufügen. Dies lädt sie alle mit Dot-Sourcing:

dir test*.ps1 | % {. $_.FullName}

Dies lädt sie alle, indem sie zuerst ihren Inhalt lesen:

dir test*.ps1 | % {iex (gc $_.FullName -Raw)}

Jetzt müssen wir uns genauer ansehen, wie PowerShell funktioniert. Ich mag JetBrains dotPeek für einen Decompiler. Wenn Sie jemals versucht haben, PowerShell in eine .NET-Anwendung einzubetten , werden Sie feststellen, dass die Assembly die meisten relevanten Elemente enthält System.Management.Automation. Dekompilieren Sie dieses in ein Projekt und einen PDB.

Um zu sehen, wo all diese mysteriöse Zeit verbracht wird, verwenden wir einen Profiler. Ich mag die in Visual Studio integrierte. Es ist sehr einfach zu bedienen . Fügen Sie den Ordner mit dem PDB zu den Symbolpositionen hinzu . Jetzt können wir eine Profilerstellung für eine PowerShell-Instanz durchführen, die nur eines der Testskripts ausführt. (Legen Sie die Befehlszeilenparameter fest, die -Filemit dem vollständigen Pfad des ersten Skripts verwendet werden sollen. Legen Sie den Startpfad auf den Ordner fest, der alle kleinen Skripts enthält.) Öffnen Sie anschließend die Eigenschaften des powershell.exeEintrags unter Ziele, und ändern Sie sie die Argumente, um das andere Skript zu verwenden. Klicken Sie dann mit der rechten Maustaste auf das oberste Element im Leistungs-Explorer und wählen Sie Profilerstellung starten. Der Profiler wird erneut mit dem anderen Skript ausgeführt. Jetzt können wir vergleichen. Stellen Sie sicher, dass Sie auf "Show All Code" klicken, wenn die Option angegeben ist. Für mich wird dies in einem Benachrichtigungsbereich in der Zusammenfassungsansicht des Beispielprofilberichts angezeigt.

Die Ergebnisse kommen herein

Auf meinem Computer hat die Get-ContentVersion 9 Sekunden gebraucht, um die 2000 Skriptdateien durchzugehen. Die wichtigen Funktionen auf dem "Hot Path" waren:

Microsoft.PowerShell.Commands.GetContentCommand.ProcessRecord
Microsoft.PowerShell.Commands.InvokeExpressionCommand.ProcessRecord

Dies ist sehr sinnvoll: Wir müssen warten, Get-Contentbis der Inhalt von der Festplatte gelesen wurde, und wir müssen warten, bis wir Invoke-Expressiondiesen Inhalt nutzen können.

In der Dot-Source-Version brauchte mein Computer etwas mehr als 15 Sekunden, um diese Dateien zu verarbeiten. Diesmal waren die Funktionen auf dem Hot Path native Methoden:

WinVerifyTrust
CodeAuthzFullyQualifyFilename

Die zweite scheint nicht dokumentiert zu sein, WinVerifyTrustführt jedoch eine Vertrauensüberprüfungsaktion für ein bestimmtes Objekt durch. Das ist ungefähr so ​​vage wie möglich, aber mit anderen Worten, diese Funktion überprüft die Authentizität einer bestimmten Ressource mithilfe eines bestimmten Anbieters. Beachten Sie, dass ich keine ausgefallenen Sicherheitsfunktionen für PowerShell aktiviert habe und meine Skriptausführungsrichtlinie lautet Unrestricted.

Was das heißt

Kurz gesagt, Sie warten darauf, dass jede Datei auf irgendeine Weise überprüft und möglicherweise auf eine Signatur überprüft wird, obwohl dies nicht erforderlich ist, wenn Sie die ausgeführten Skripts nicht einschränken. Wenn Sie gcund dann iexder Inhalt, ist es, als hätten Sie die Funktionen an der Konsole eingegeben, so dass es keine Ressource gibt, die überprüft werden muss.

Ben N
quelle
2
Ben, danke für diese großartige Antwort. Beeindruckt, dass Sie bis zum Dekompilieren gegangen sind, was einen Schritt über alles hinaus darstellt, was ich versucht habe. Ich werde sehen, ob es eine Möglichkeit gibt, Ihre Testmethode auf einer der Maschinen zu befolgen, auf denen dieses Problem am akutesten ist. Dies kann eine Weile dauern, also halten Sie nicht den Atem an!
Charlie Joynt