Skip to content
AgileLogics
6 min read

Performance

Prerequisites: Using .NET directly

PowerShell is fast enough for most automation, but a few patterns make scripts dramatically slower. Measure before and after with Measure-Command.

1. Don't grow arrays with +=

$slow = Measure-Command {
  $items = @()
  foreach ($i in 1..30000) { $items += $i }
}
$fast = Measure-Command {
  $items = foreach ($i in 1..30000) { $i }
}
$slow.TotalMilliseconds -gt ($fast.TotalMilliseconds * 10)
True

Each += copies the whole array into a new one, so the cost grows with the square of the size: at 10,000 items the difference is small, but at 50,000 items += took about 8 seconds on the test machine against 70 milliseconds for assigning the loop's output. Assign the loop's output, or use a List[T] when items are added from several places.

2. Filter left

Filter at the source (-Filter, -Name, server-side query parameters) rather than retrieving everything and using Where-Object.

3. Prefer language features in hot loops

Slower Faster
$items | ForEach-Object { } foreach ($i in $items) { }
$items | Where-Object { } $items.Where({ }) or foreach + if
| Out-Null $null = ... or [void]
Where-Object lookup inside a loop a hash table built once
string += in a loop -join or StringBuilder
$numbers = 1..10
$numbers.Where({ $_ % 3 -eq 0 }) -join ','
$numbers.Where({ $_ -gt 4 }, 'First', 2) -join ','
$numbers.ForEach({ $_ * 10 })[0..2] -join ','
3,6,9
5,6
10,20,30

The .Where() and .ForEach() methods run faster than the cmdlets, and .Where() has modes like First, Last, and Split.

4. Hash tables for lookups

$orders = 1..5000 | ForEach-Object { [pscustomobject]@{ Id = $_; CustomerId = $_ % 100 } }
$customers = 0..99 | ForEach-Object { [pscustomobject]@{ Id = $_; Name = "Customer $_" } }

$byId = @{}
foreach ($c in $customers) { $byId[$c.Id] = $c }

$joined = foreach ($o in $orders) {
  [pscustomobject]@{ Order = $o.Id; Customer = $byId[$o.CustomerId].Name }
}
$joined[41]

Order Customer
----- --------
   42 Customer 42

A hash-table lookup takes constant time; $customers | Where-Object Id -eq $o.CustomerId inside the loop would scan all 100 customers 5,000 times.

5. Stream large files

Get-Content -ReadCount 1000 reads in batches; for very large files, [System.IO.File]::ReadLines($path) streams lines with far less overhead.

Profile first

Find the slow part before optimizing. Measure-Command each section, or use the Profiler module (Trace-Script) for a line-by-line breakdown.