A PowerShell script builds a result set with `$results += $item` inside a loop over 100,000 records and slows to a crawl while memory climbs. What is happening, and what should it use instead?
answer
- it degrades as it grows, rather than being uniformly slow
- the underlying .NET type cannot grow
- every iteration copies what came before
- doubling buffer versus copy-every-time
- the loop itself can be assigned
basics
~20 sPowerShell arrays are fixed-size .NET arrays, so each += allocates a new array one element longer and copies every existing element — quadratic work plus heavy garbage. Use a List[T] with .Add(), or let the loop stream its output and assign the whole loop once.
solid answer
~50 s`$results` is a `System.Object[]`, and .NET arrays cannot grow. Every `+=` therefore allocates a fresh array of `Count + 1`, copies all existing elements into it, appends the new one, and abandons the old array to the garbage collector. Over n iterations that is roughly n²/2 element copies and n discarded arrays, so 100,000 items means billions of copies and constant GC pressure — the loop starts fast and visibly degrades. Two good fixes. Use a growable collection: `$results = [System.Collections.Generic.List[object]]::new()` then `$results.Add($item)`, which is amortised O(1) because the list doubles its backing buffer. Or drop the accumulator entirely and assign the loop itself — `$results = foreach ($r in $records) { Transform $r }` — which lets PowerShell collect the output efficiently in one pass. Best of all, if the consumer is downstream, emit each item and never accumulate.
code
powershell · 12 lines$records = 1..100000
# quadratic: each += allocates a new array and copies every element
$slow = @()
foreach ($r in $records) { $slow += $r }
# amortised O(1) appends
$fast = [System.Collections.Generic.List[int]]::new()
foreach ($r in $records) { $fast.Add($r) }
# no accumulator at all - assign the loop itself
$idiomatic = foreach ($r in $records) { $r }go deeper
Know that += on an array copies the whole array each time, and that a List with .Add() is the growable alternative.
Explain the quadratic cost concretely — n(n-1)/2 element copies plus n discarded arrays — and show both fixes: a generic List, or assigning the foreach loop directly.
Diagnose from the symptom before touching the code: throughput degrading as the collection grows points at an accumulator, and the strongest fix is often to stop accumulating and stream instead.
Frame it as a scale-limit review rule: flag unbounded accumulation in shared tooling, since the code that works on today's dataset silently sets the ceiling for tomorrow's.
## Why `+=` on an array is quadratic A PowerShell array is not a list. `$results = @()` creates a `System.Object[]` — a plain .NET array — and .NET arrays have a length fixed at allocation. There is no append operation to call. So when you write `$results += $item`, PowerShell does the only thing it can: 1. allocate a new array of length `Count + 1`; 2. copy every element from the old array into the new one; 3. place `$item` in the last slot; 4. rebind `$results` to the new array; 5. leave the old array for the garbage collector. The copy in step 2 is the problem. On iteration 1 it copies 0 elements, on iteration 2 it copies 1, on iteration n it copies n−1. Summed over n iterations that is n(n−1)/2 — for 100,000 items, about five billion element copies. It also allocates 100,000 arrays whose average size is 50,000 references, which is why memory climbs and the GC runs constantly. The characteristic symptom is **degradation, not slowness**: the first thousand items fly past, the last thousand crawl. That signature — throughput falling as the collection grows — is what should make you look for an accumulator in the loop. ## Fix one: a growable collection ```powershell $results = [System.Collections.Generic.List[object]]::new() foreach ($r in $records) { $results.Add((Transform $r)) } ``` `List<T>` keeps a backing array with spare capacity and doubles it when full. Each `Add` is amortised O(1), and the total copying across n adds is O(n) rather than O(n²). Type it when you can — `List[string]`, `List[int]`, `List[pscustomobject]` — to avoid boxing and to make the intent explicit. Two details worth remembering. `.Add()` on a generic `List` returns `void`, so it does not pollute your output stream; the older `[System.Collections.ArrayList]` returns the new index and needs `$null = $list.Add(...)`. And when you pass a `List` down a pipeline, PowerShell unrolls it just as it would an array, so callers see no difference. ## Fix two: let the loop be the expression Often the accumulator is unnecessary. `foreach`, `for`, `while` and `if` are all expressions in PowerShell — their output can be assigned: ```powershell $results = foreach ($r in $records) { Transform $r } ``` Everything each iteration emits flows into one collection that the engine builds efficiently in a single pass. There is no repeated reallocation, the code is shorter, and there is no accumulator variable to get wrong. This is the idiomatic PowerShell form and usually the one to reach for first. ## Fix three: do not accumulate at all If the result set exists only to be piped somewhere, building it in memory is pure cost. Emit each item as you produce it — from a `process` block if the code is a pipeline-capable function, or from the loop body if it is a script — and let the consumer work as the data arrives. Peak memory stays flat regardless of record count, and the first downstream object appears immediately instead of after the last input. This is the difference between an export that handles a million rows on a small runner and one that has to be re-architected when the dataset grows. ## What about `ArrayList`? `[System.Collections.ArrayList]` also grows in amortised constant time and is still common in older scripts. It predates generics, stores everything as `object`, and its `Add` returns an index you must suppress. There is no reason to choose it for new code over `List[T]`, but recognise it — and do not "fix" it by converting to `+=`. ## The reviewer's heuristic Any `+=` on a collection inside a loop is a smell worth a comment, and a defect worth blocking when the loop bound is unbounded or data-driven. For a fixed loop of twenty configuration entries it is harmless and arguably clearer. The judgment is about how large n can become, not about the operator being forbidden — and the honest senior answer says so rather than reciting a rule.
- How would you confirm this diagnosis rather than assume it?Time the loop at increasing sizes — 10k, 20k, 40k. Linear behaviour roughly doubles the time each step; quadratic roughly quadruples it, which identifies the accumulator without any profiler. `Measure-Command` around the loop is enough. Watching the process's memory climb and fall in sawtooth as the GC reclaims abandoned arrays corroborates it.
- Why does `$results = foreach (...) { ... }` avoid the problem?Because there is no repeated reassignment. The loop is an expression whose emitted objects the engine gathers into a single collection in one pass, so nothing is reallocated per iteration. It also removes the accumulator variable entirely, which is one fewer thing to initialise wrongly or forget to reset.
- When is `+=` on a collection acceptable?When n is small and bounded — a handful of configuration entries, a few command-line arguments — where the clarity of one operator beats introducing a List. The rule is about growth, not about the operator: if the loop bound comes from data whose size you do not control, it is a defect; if it is a literal list of five things, it is fine.
saying these in an interview costs you the question
- Blames PowerShell being 'interpreted and slow' generally
- Thinks += appends in place and the cost is constant
- Suggests Out-File or a database as the fix without addressing the accumulator
- Cannot explain why the loop starts fast and gets slower
- Converts to ArrayList and forgets to suppress the returned index