Maksim Kabakou - Fotolia
PowerShell ForEach-Object cmdlet picks up speed
Administrators get extra incentive to switch to PowerShell 7 after an existing cmdlet acquires a performance enhancement to run multiple iterations in parallel.
Since its move to an open source project in 2016, PowerShell's development picked up significantly.
The PowerShell 7.0 release arrived in March with a slew of improvements and new features. One of the most intriguing updates occurred with the PowerShell ForEach-Object cmdlet, which gained a powerful new ability to perform loops in parallel.
Most system administrators have needed to execute some command or operation on multiple systems. Before the addition of the Parallel parameter, each iteration in a loop would run sequentially, or one after another. While this may work fine for loops with limited items, loops that require each step to take substantially more time is a perfect candidate for the Parallel parameter.
The PowerShell ForEach-Object Parallel parameter attempts to run multiple iterations of the loop at the same time, potentially saving on the overall runtime. With this newfound capability, there are several important caveats to understand before implementing the Parallel in any production scripts.
Understanding PowerShell ForEach-Object -Parallel
PowerShell supports several different methods of parallelism. In the case of ForEach-Object, runspaces provides this functionality. Runspaces are separate threads in the same process. These threads have less overhead compared to PowerShell jobs or PowerShell remoting.
A few factors will add to the amount of overhead used with the ForEach-Object Parallel parameter. You will need to import additional modules and reference outside variables with the $Using: syntax. In some situations, the Parallel parameter is not ideal due to the extra overhead it generates when in use, but there is a way to shift that burden away from the source machine.
One automation concern with this additional feature is flooding your infrastructure or servers with multiple operations at once. To control this behavior, the ThrottleLimit parameter restricts the number of concurrent threads. When one thread completes, any additional iterations will take that thread's place, up to the defined limit.
The default ThrottleLimit is five threads, which generally keeps memory and CPU usage low. Without this setting, you can quickly overwhelm your local system or server by running too many threads in parallel.
Finally, one other useful ability of the Parallel parameter is it allows any parallel loops to run as PowerShell jobs. This functionality lets the PowerShell ForEach-Object command return a job object, which you can retrieve at a later time.
Performance between Windows PowerShell 5.1 and PowerShell 7
There have been many performance improvements since Windows PowerShell 5.1 and especially so with the latest release of PowerShell 7. Specifically, how have things improved with the development of the ForEach-Object command?
The code below runs a simple test to show the speed difference in the PowerShell ForEach-Object command between different versions of PowerShell. The first example shows results from Windows PowerShell 5.1:
$Collection = 1..10000
(Measure-Command {
$Collection | ForEach-Object {
$_
}
}).TotalMilliseconds
# Result: 35112.3222
In that version, the script takes more than 35 seconds to finish. In PowerShell 7, the difference is dramatic and takes slightly more than 1 second to complete:
$Collection = 1..100000
(Measure-Command {
$Collection | ForEach-Object {
$_
}
}).TotalMilliseconds
# Result: 1042.3588
How else can we demonstrate the power of the Parallel parameter? One common feature in PowerShell scripts used in production is to introduce a delay to allow some other action to complete first. The following script uses the Start-Sleep command to add this pause.
$Collection = 1..10
(Measure-Command {
$Collection | ForEach-Object {
Start-Sleep -Seconds 1
$_
}
}).TotalMilliseconds
# Result: 10096.1418
As expected, running sequentially, the script block takes almost 10 seconds. The following code demonstrates the same loop using the Parallel parameter.
$Collection = 1..10
(Measure-Command {
$Collection | ForEach-Object -Parallel {
Start-Sleep -Seconds 1
$_
}
}).TotalMilliseconds
# Result: 2357.487
This change shaved almost 8 seconds off the total runtime. Even with only five threads running at once, each iteration kicks off when the previous one completes for a significant reduction in execution time.
Putting the Parallel parameter in action
How can these enhancements and abilities translate to real-world system administration actions? There are countless scenarios that would benefit from running operations in parallel, but two that are very common are retrieving information from multiple computers and running commands against multiple computers.
Collecting data from multiple computers
One common administrative task is to gather information on many different systems at once. How is this done with the new PowerShell ForEach-Object -Parallel command? The following example retrieves the count of files in user profiles remotely across systems.
$Computers = @(
"Computer1"
"Computer2"
"Computer3"
"Computer4"
"Computer5"
)
(Measure-Command {
$User = $Env:USERNAME
$Computers | ForEach-Object -Parallel {
Invoke-Command -ComputerName $_ -ScriptBlock {
Write-Host ("{0}: {1}" -F $_, (Get-ChildItem -Path "C:\Users\$($Using:User)" -Recurse).Count)
}
}
}).TotalMilliseconds
Computer1: 31716
Computer2: 30055
Computer4: 28542
Computer3: 33556
Computer5: 26052
13572.8172
On PowerShell 7, the script completes in just over 13 seconds. The same script running on Windows PowerShell 5.1 without the Parallel parameter executes in just over 50 seconds.
Running commands against multiple computers
Oftentimes, an administrator will need a command or series of commands to run against several target systems as fast as possible. The following code uses the Parallel parameter and PowerShell remoting to make quick work of this transfer process.
$Computers = @(
"Computer1"
"Computer2"
"Computer3"
"Computer4"
"Computer5"
)
$RemoteFile = "\\Server1\SharedFiles\Deployment.zip"
(Measure-Command {
$Computers | ForEach-Object -Parallel {
Invoke-Command -ComputerName $_ -ScriptBlock {
Copy-Item -Path $Using:RemoteFile -Destination "C:\"
}
}
}).TotalMilliseconds
23572.8172
Shifting overhead with Invoke-Command
One useful feature in PowerShell when working with remote systems is to lower overhead by shifting computer-intensive commands to the target system. In the previous example, Invoke-Command runs the commands via the local PowerShell session on the remote systems. This is a helpful way to spread the overhead load and avoid potential bottlenecks in performance.