PowerShell Performance: Filtering Collections

Depending on what version of Windows PowerShell you are running, there are several different methods and syntax available for filtering collections. The differences are not just aesthetic; each one has different capabilities and performance characteristics. If you find yourself needing to optimize the performance of a script that processes a large amount of data, it helps to know what your options are. I ran some tests on the performance of various filtering techniques, and here are the results:

Description Syntax Pros Cons
Where-Object (-FilterScript)
Get-Process | Where-Object { $_.Name -eq 'powershell_ise' }
  • Runs on any version of Windows PowerShell.
  • Streams objects via the pipeline, keeping memory utilization to a minimum.
  • Any complex logic can be implemented inside the script block.
  • Slowest execution time of all the available options.
PowerShell 3.0 Simplified Where-Object syntax
Get-Process | Where Name -eq 'powershell_ise'
  • Executes slightly faster than the -FilterScript option.
  • Streams objects via the pipeline, keeping memory utilization to a minimum.
  • Limited to very simple comparisons of a single property on the piped objects. No compound expressions or data transformations are allowed.
  • Only works with PowerShell 3.0 or later
PowerShell 4.0 .Where() method syntax
(Get-Process).Where({ $_.Name -eq 'powershell_ise' })
  • Much faster than both of the Where-Object cmdlet versions (about 2x the speed.)
  • Any complex logic can be implemented inside the script block.
  • Only usable on collections that are completely stored in memory; cannot stream objects via the pipeline.
  • Only works with PowerShell 4.0
PowerShell filter
filter isISE { if ($_.Name -eq 'powershell_ise') { $_ } }

Get-Process | isISE
  • Runs on any version of Windows PowerShell.
  • Streams objects via the pipeline, keeping memory utilization to a minimum.
  • Any complex logic can be implemented inside the script block.
  • Faster than all of the Where-Object and .Where() options.
  • Not as easy to read and debug; user must scroll to wherever the filter is defined to see the actual logic.
foreach loop with embedded conditionals
foreach ($process in (Get-Process)) {
    if ($process.Name -eq 'powershell_ise') {
        $process
    }
}
  • Faster than any of the previously mentioned options.
  • Any complex logic can be implemented inside the loop / conditional.
  • Only usable on collections that are completely stored in memory; cannot stream objects via the pipeline.

To test the execution speed of each option, I ran this code:

$loop = 1000

$v2 = (Measure-Command {
    for ($i = 0; $i -lt $loop; $i++)
    {
        Get-Process | Where-Object { $_.Name -eq 'powershell_ise' }
    }
}).TotalMilliseconds

$v3 = (Measure-Command {
    for ($i = 0; $i -lt $loop; $i++)
    {
        Get-Process | Where Name -eq 'powershell_ise'
    }
}).TotalMilliseconds

$v4 = (Measure-Command {
    for ($i = 0; $i -lt $loop; $i++)
    {
        (Get-Process).Where({ $_.Name -eq 'powershell_ise' })
    }
}).TotalMilliseconds

$filter = (Measure-Command {
    filter isISE { if ($_.Name -eq 'powershell_ise') { $_ } }
    
    for ($i = 0; $i -lt $loop; $i++)
    {
        Get-Process | isISE
    }
}).TotalMilliseconds

$foreachLoop = (Measure-Command {
    for ($i = 0; $i -lt $loop; $i++)
    {
        foreach ($process in (Get-Process))
        {
            if ($process.Name -eq 'powershell_ise')
            {
                # Do something with $process
                $process
            }
        }
    }
}).TotalMilliseconds

Write-Host ('Where-Object -FilterScript:  {0:f2} ms' -f $v2)
Write-Host ('Simplfied Where syntax:      {0:f2} ms' -f $v3)
Write-Host ('.Where() method:             {0:f2} ms' -f $v4)
Write-Host ('Using a filter:              {0:f2} ms' -f $filter)
Write-Host ('Conditional in foreach loop: {0:f2} ms' -f $foreachLoop)

<#
Results:

Where-Object -FilterScript:  3035.69 ms
Simplfied Where syntax:      2855.33 ms
.Where() method:             1445.21 ms
Using a filter:              1281.13 ms
Conditional in foreach loop: 1073.14 ms
#>
Posted in:
About the Author

Dave Wyatt

Profile photo of Dave Wyatt

Dave Wyatt is a Microsoft MVP (PowerShell) and a member of PowerShell.org's Board of Directors.

2 Comments

  1. And of course, there's the added complexity of cmdlets that support their own -filter parameter. That is *usually* passed to the underlying technology and filtered at the source, which is often the fastest of all. As an example:

    Get-ADUser -filter { cn -like 'd*' }

    Would be faster than getting all users and then using any of the above filtering techniques. With this example, ONLY the desired results are returned from AD, so no PowerShell-side filtering is necessary. That lowers load on the DC, lessens the traffic transmitted from the DC, and lowers PowerShell's memory consumption. It's also faster, since the shell doesn't have to enumerate anything at all. When possible, it's typically preferable to get filtering over and done with as early as possible - e.g., at the source or close to it - rather than making PowerShell do the filtering.

    Of course, it isn't always possible. Not every cmdlet supports filtering, and even when they do, sometimes the filtering supported by the cmdlet won't get you want you want. In those cases, you'll want to select a filtering option using the excellent criteria Dave pointed out.

  2. I had to try this out, and it seems that another thing impact the speed.
    running the script in ISE, with a script panel that is saved and one that is unsaved:
    Unsaved:
    Where-Object -FilterScript: 5713,19 ms
    Simplfied Where syntax: 5502,67 ms
    .Where() method: 3451,77 ms
    Using a filter: 3172,18 ms
    Conditional in foreach loop: 3120,57 ms

    Saved:
    Where-Object -FilterScript: 7654,08 ms
    Simplfied Where syntax: 7544,80 ms
    .Where() method: 5610,05 ms
    Using a filter: 5230,60 ms
    Conditional in foreach loop: 4630,36 ms

    Weird stuff.