Latest posts by Brandon Olin (see all)
- The Operation Validation Framework: Test your infrastructure using Pester - Mon, Jun 25 2018
- How to write an Azure Function in PowerShell - Tue, Jun 12 2018
- Process file paths from the pipeline in PowerShell functions - Mon, Jun 4 2018
PowerShell does a lot for us to make our scripting lives easier. This is one reason it's such a popular language in the Windows ecosystem. That flexibility and ease of use can sometimes come at a cost though.
How you write PowerShell can have a significant impact on performance. What seems innocuous when executed once can have a huge impact when executed hundreds or thousands of times in a loop or as part of a bigger script run against many machines.
Let's go over two of the most common reasons your PowerShell script may be running slow.
Arrays and the += assignment operator ^
It's very common to see a pattern like this in PowerShell. We define a result collection, loop over a large list of things, perform some type of process or calculation on it, and then add the finished item to the result collection. The benefit to this pattern is it is very easy to read and understand what is going on logically. Not all is as it seems though, and there are some serious performance penalties lurking in this pattern.
Let's run this again but wrap it with Measure-Command to see how long it takes.
Yikes! Performing this simple task took nearly four seconds. We know PowerShell can do better than that. Let's see why this took so long.
In PowerShell, and C# by the way, arrays are immutable, meaning they have a fixed size at creation time. When you use +=, you think you are appending to an existing array, but PowerShell is creating a new fixed-sized array with the values from the old array and the item you are adding. It then removes the old variable from memory. This is an expensive operation, both in terms of memory use and computation time. For small loops, you will probably not notice this performance hit. Only when performing this operation hundreds or thousands of times would you usually see this time and resource killer creep up.
.NET to the rescue ^
Fortunately, we have a set of .NET collection types we can use to speed this up. The most commonly used one with PowerShell is probably System.Collection.ArrayList. Internally, ArrayList uses a more efficient algorithm to increase the size of the list when needed. Here is how that same operation would look when using the ArrayList collection type.
You can see the code is nearly identical to the example above. Note that we're redirecting the output of the Add() method to $null. The Add() method returns the newly added items' index in the ArrayList. You may not want that output cluttering up your output stream, so we can redirect it to $null or capture it in a variable for later use.
Let's see how long this new method took.
Using System.Collection.ArrayList took about 224 milliseconds. That is quite a difference!
Note: To add type safety, you can also use the strongly typed System.Collection.Generic.List collection. This collection type has a slight speed improvement over ArrayList as well.
An alternative to using the .NET types is to assign the values output from the loop to a variable directly. PowerShell will collect all the items in the loop and assign them to $result as one operation.
Now let's see how long this operation took.
This one took about 232 milliseconds. That time is close to using ArrayList.
Always filter left ^
Filtering data returned from cmdlets or functions is another common PowerShell pattern. Suppose we want to search the local event log for a certain event ID. A common approach you may see is something like this:
This is a slow operation since we're sending each event returned from Get-WinEvent down the pipeline and only then filtering with Where-Object. The pipeline will not finish until it processes every event log entry Get-WinEvent returns.
Let's wrap this in Measure-Command to see how long it takes.
This operation took 2.6 seconds. Let's try to speed that up.
A better approach is to filter left as much possible using the built-in parameters of many cmdlets to return only data that passes the filter. This is the "filter left, format right" saying you may have heard about in PowerShell circles. For Get-WinEvent, we'll use the -FilterHashtable parameter to pass in our filters rather than piping all items to Where-Object.
Interesting—this took .08 seconds vs. 2.61 seconds. In this example, -FilterHashtable is over 31 times faster!
As an exercise, try searching for other cmdlets and functions that have built-in filtering functionality by using Get-Command.
As you can see, simply following these two performance optimizations can lead to dramatic speed increases.