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.