Improvements in ForEach and ForEach-Object
Windows PowerShell users are well versed in the use of both the ForEach
statement and the ForEach-Object
cmdlet. You can use both of these methods in your scripts to process collections, such as all the users in a specific Active Directory group, or the audio files in a file share. In PowerShell 7, both of these iteration methods are considerably faster.
Using either ForEach
mechanism is a quick and easy way of processing a collection. One downside some IT pros may have noticed is that the overhead of ForEach
processing in Windows PowerShell grows with the size of the collection. With small collection sizes, you are not likely to notice any difference. As the collection size grows, so does the overhead.
Getting ready
You run this recipe on SRV1
after you have installed PowerShell 7 and have created a console profile file. Run this recipe in an elevated console.
How to do it...
- Creating a remoting connection to the localhost using Windows PowerShell
New-PSSession -UseWindowsPowerShell -Name 'WPS'
- Getting a remoting session
$Session = Get-PSSession -Name 'WPS'
- Checking the version of PowerShell in the remoting session
Invoke-Command -Session $Session  -ScriptBlock {$PSVersionTable}
- Defining a long-running script block using
ForEach-Object
$SB1 = {   $Array  = (1..10000000)   (Measure-Command {     $Array | ForEach-Object {$_}}).TotalSeconds }
- Running the script block locally:
[gc]::Collect() $TimeInP7 = Invoke-Command -ScriptBlock $SB1 "Foreach-Object in PowerShell 7.1: [{0:n4}] seconds" -f $TimeInP7
- Running the script block in PowerShell 5.1
[gc]::Collect() $TimeInWP  = Invoke-Command -ScriptBlock $SB1 -Session $Session "ForEach-Object in Windows PowerShell 5.1: [{0:n4}] seconds" -f $TimeInWP
- Defining another long-running script block using
ForEach
$SB2 = {     $Array  = (1..10000000)     (Measure-Command {       ForEach ($Member in $Array) {$Member}}).TotalSeconds }
- Running it locally in PowerShell 7
[gc]::Collect() $TimeInP72 = Invoke-Command -ScriptBlock $SB2 "Foreach in PowerShell 7.1: [{0:n4}] seconds" -f $TimeInP72
- Running it in Windows PowerShell 5.1
[gc]::Collect() $TimeInWP2  = Invoke-Command -ScriptBlock $SB2 -Session $Session "Foreach in Windows PowerShell 5.1: [{0:n4}] seconds" -f $TimeInWP2
How it works…
In step 1, you use New-PSSession
to create a remoting session using a Windows PowerShell endpoint. This step produces output like this:
Figure 2.24: Creating a remoting connection to the localhost
In step 2, you get the session object representing the session you created in the previous step. This creates no output.
In step 3, you obtain the version of PowerShell that the remoting session is using to process commands, namely, Windows PowerShell 5.1. The output of this step looks like this:
Figure 2.25: Checking the PowerShell version in the remoting session
In step 4, you create a script block, $SB1
, which uses the ForEach-Object
cmdlet to iterate over a large collection. This step creates no output.
You invoke the $SB1
script block in the local session, in step 5. This step runs the script block in PowerShell 7. The output from this step looks like this:
Figure 2.26: Running the script block locally
With step 6, you run the $SB1
script block in Windows PowerShell 5.1, which produces output like this:
Figure 2.27: Running the script block in PowerShell 5.1
You next create a script block that makes use of the ForEach
syntax item, in step 7, producing no output. You then run this second script block in PowerShell 7, in step 8, which produces output like this:
Figure 2.28: Running the script block locally in PowerShell 7
In the final step, step 9, you run $SB1
in the remoting session created earlier (in other words, in Windows PowerShell 5.1), which produces output like this:
Figure 2.29: Running the script block in PowerShell 5.1
There's more...
In step 1, you create, implicitly, a remoting session to the localhost using a process transport that is much faster than the traditional remoting session using WinRM.
In steps 5, 6, 8, and 9, you force .NET to perform a garbage collection. These are steps you can use to minimize the performance hits of running a script block in a remote session (in Windows PowerShell) and to reduce any impact of garbage collections while you are performing the tests in this recipe.
As you can see from the outputs, running ForEach-Object
is much faster in PowerShell 7, as is running ForEach
in PowerShell 7. Processing large collections of objects is a lot faster in PowerShell 7.
The improvements to loop processing that you can see in the recipe, combined with the use of ForEach-Object -Parallel
you saw in Exploring parallel processing with ForEach-Object, provide an excellent reason to switch to PowerShell 7 for most operations.
The performance of iterating through large collections is complex. You can read an excellent article on this subject at https://powershell.one/tricks/performance/pipeline. This article also addresses the performance of using the pipeline versus using ForEach
to iterate across collections in a lot more detail.