Make Your PowerShell For Loops 4x Faster

Looping through information is a fundamental task. It is done many times a day by many people in many ways on computers. In PowerShell, one way to loop is using a For loop (see Slow Loop).

Hidden in this simple example is a performance hit where looping over 100,000 items has a 2 second response time rather than subsecond. That’s the difference of hitting enter and getting a prompt compared to counting 1 one-thousand, 2 one-thousand.

Slow Loop

$range = 1..100000            

For($i=0; $i -lt $range.Count; $i++) {
    $i
}

A Single Line Change

If you needed to do the same loop 5 times in your script, you could speed it up so the entire script runs faster than executing the first loop.

You do this by storing (caching) the array count in a variable and using it in the for loop.

4x Faster

$range = 1..100000            

$count = $range.Count
For($i=0; $i -lt $count; $i++) {
    $i
}

Good Habits

Making this a habit is good practice. It eliminates waste and enables your scripts be faster when lots of information is thrown at it when you least expect it.

More Results

Test                Range TotalSeconds
----                ----- ------------
Test-WithoutCache      10     0.000339
Test-WithCache         10     0.000201
Test-WithoutCache     100     0.001526
Test-WithCache        100     0.000478
Test-WithoutCache    1000     0.017743
Test-WithCache       1000     0.003395
Test-WithoutCache   10000     0.139237
Test-WithCache      10000     0.033718
Test-WithoutCache  100000     1.410456
Test-WithCache     100000     0.358698
Test-WithoutCache 1000000    14.398003
Test-WithCache    1000000     3.835144

Test Harness

Run the tests and read the script. Notice how the functions are stored in an array using the custom function ql. Extending this harness to handle more tests or different ranges is very simple.

Each test is executed, timed, formatted and cast when the new PSObject property TotalSeconds is created.

TotalSeconds = [Double]("{0:#0.#####0}" -f
    (Measure-Command { & $test (1..$range) }).TotalSeconds)
Function Test-WithoutCache ($data) {


    $sum = 0
    # antipattern
    # access the array count on each iteration
    for($idx = 0 ; $idx -lt $data.count; $idx++) {
        $sum+=$data[$idx]
    }
    $sum
}            

Function Test-WithCache ($data) {
    # capture the count; cache it
    $count = $data.count
    $sum = 0
    # use the $count variable in the
    # for loop and improve performance 4x
    for($idx = 0 ; $idx -lt $count; $idx++) {
        $sum+=$data[$idx]
    }
    $sum
}            

function ql {$args}            

$tests  = ql Test-WithoutCache Test-WithCache
$ranges = ql 10 100 1000 10000 100000 1000000            

$(ForEach($range in $ranges) {
    ForEach($test in $tests) {
        $msg = ("[{0}] Running {1} with {2} items" -f  (Get-Date), $test, $range)
        Write-Host -ForegroundColor Green $msg
        New-Object PSObject -Property @{
            TotalSeconds = [Double]("{0:#0.#####0}" -f (Measure-Command { & $test (1..$range) }).TotalSeconds)
            Range  = $range
            Test   = $test
        } | Select Test, Range, TotalSeconds
    }
}) | Format-Table -AutoSize