Setting up science
First, some scripts to help us test this. This generates 2000 script files, each with a single small function:
1..2000 | % { "Function Test$_(`$someArg) { Return `$someArg * $_ }" > "test$_.ps1" }
That ought to be enough to make the normal startup overhead not matter too much. You can add more if you like. This loads them all using dot-sourcing:
dir test*.ps1 | % {. $_.FullName}
This loads them all by reading their contents first:
dir test*.ps1 | % {iex (gc $_.FullName -Raw)}
Now we need to do some serious inspection of how PowerShell works. I like JetBrains dotPeek for a decompiler. If you've ever tried to embed PowerShell in a .NET application, you'll find that the assembly that includes most of the relevant stuff is System.Management.Automation
. Decompile that one into a project and a PDB.
To see where all this mysterious time is being spent, we'll use a profiler. I like the one built into Visual Studio. It's very easy to use. Add the folder containing the PDB to the symbol locations. Now, we can do a profiling run of a PowerShell instance that just runs one of the test scripts. (Set the command-line parameters to use -File
with the full path of the first script to try. Set the startup location to the folder containing all the tiny scripts.) Once that one is done, open the Properties on the powershell.exe
entry under Targets and change the arguments to use the other script. Then right-click the topmost item in Performance Explorer and choose Start Profiling. The profiler runs again using the other script. Now we can compare. Make sure you click "Show All Code" if given the option; for me, that shows up in a Notifications area in the Summary view of the Sample Profiling Report.
The results come in
On my machine, the Get-Content
version took 9 seconds to go through the 2000 script files. The important functions on the "Hot Path" were:
Microsoft.PowerShell.Commands.GetContentCommand.ProcessRecord
Microsoft.PowerShell.Commands.InvokeExpressionCommand.ProcessRecord
This makes a lot of sense: we have to wait for Get-Content
to read the content from disk, and we have to wait for Invoke-Expression
to make use of those contents.
On the dot-source version, my machine spent a little over 15 seconds to work through those files. This time, the functions on the Hot Path were native methods:
WinVerifyTrust
CodeAuthzFullyQualifyFilename
The second one there appears to be undocumented, but WinVerifyTrust
"performs a trust verification action on a specified object." That's about as vague as you can get, but in other words, that function verifies the authenticity of a given resource using a given provider. Note that I haven't enabled any fancy security stuff for PowerShell, and my script execution policy is Unrestricted
.
What that means
In short, you're waiting for each file to be verified in some way, probably checked for a signature, even though that's not necessary when you don't restrict the scripts that are allowed to run. When you gc
and then iex
the contents, it's like you had typed the functions at the console, so there's no resource to verify.
2Ben, thanks for this superb answer. Impressed that you went as far as de-compiling, which is a step beyond anything I have tried. I will see if there is any way I can follow your testing method on one of the machines where this problem is most acute. This could take a long while so don't hold your breath! – Charlie Joynt – 2017-01-25T10:40:14.250