Tag Archives: script block

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
#>

Great Debate: Pipeline Coding Syles


A good programmer or scripter should always try to produce code that is easy to read and understand. Choosing and consistently applying a strategy for indentation and bracing will make your life, and possibly those of your co-workers, much easier. (Unless, of course, you’re that guy who prides himself in writing code that looks like a cat walked across his keyboard, and cackles any time someone tries to understand it.)

Much of PowerShell’s syntax can borrow coding conventions from C-style languages, but the pipeline is something new. Below are two pipelines formatted a few different ways. Which styles do you find to be the easiest to read? In the second pipeline, which contains an embedded multi-line script block, would your choice be any different if the script block were much longer? For instance, if you couldn’t see both the beginning and the end of the pipeline without scrolling. Do you have another way of formatting these bits of code that you like better?

Remember, there is no right or wrong answer here. The idea is just to generate discussion, and perhaps to help people produce more readable code.

# A simple pipeline with each command fitting on a single line.
# Line breaks after the pipe character.

Get-ChildItem -Path $home\Documents -File |
Where-Object { $_.LastWriteTime -lt (Get-Date).AddDays(-120) } |
Select-Object -ExpandProperty Name

Next-Command

# The same pipeline, this time indenting each line after the first one.

Get-ChildItem -Path $home\Documents -File |
    Where-Object { $_.LastWriteTime -lt (Get-Date).AddDays(-120) } |
    Select-Object -ExpandProperty Name

Next-Command

# This time, using backticks at the end of each line, and placing the 
# pipe character at the beginning of the next line.

Get-ChildItem -Path $home\Documents -File `
| Where-Object { $_.LastWriteTime -lt (Get-Date).AddDays(-120) } `
| Select-Object -ExpandProperty Name

Next-Command

# And again, with indentation

Get-ChildItem -Path $home\Documents -File `
    | Where-Object { $_.LastWriteTime -lt (Get-Date).AddDays(-120) } `
    | Select-Object -ExpandProperty Name

Next-Command

# A slightly more complex pipeline involving an embedded script block
# passed to ForEach-Object.  Does the pipe symbol after a closing brace,
# potentially much farther down in the code from where the pipeline
# started, change your opinion on what's the easiest to read?

Import-Csv -Path $inputCsvPath |
ForEach-Object {
    # Transform objects in some way
} |
Export-Csv $outputCsvPath -NoTypeInformation

Next-Command

# The same three variations of style involving indentation and backticks:

Import-Csv -Path $inputCsvPath |
    ForEach-Object {
        # Transform objects in some way
    } |
    Export-Csv $outputCsvPath -NoTypeInformation

Next-Command

Import-Csv -Path $inputCsvPath `
| ForEach-Object {
    # Transform objects in some way
} `
| Export-Csv $outputCsvPath -NoTypeInformation

Next-Command

Import-Csv -Path $inputCsvPath `
    | ForEach-Object {
        # Transform objects in some way
    } `
    | Export-Csv $outputCsvPath -NoTypeInformation

Next-Command