Scopes, dot-sourcing and performance

PowerShell · Expert · 12 min

Scope surprises cause silent bugs, and a few habits make scripts orders of magnitude faster on large data.

Each script, function and script block runs in its own scope. A child scope can read variables from its parents, but assigning creates a new local variable that hides the parent's one. To change an outer variable, use a scope modifier: $script:count (the script's top level) or $global:count (the session). $private:x hides a variable from child scopes. Modules have their own isolated session state, so their internals do not leak into yours. Dot-sourcing runs a script in the caller's scope instead of a new one.

Usually it is better to return values than to modify outer variables; scope modifiers are best kept for module-level state.

Performance habits that matter on large inputs:

  • Do not grow arrays with +=: assign the output of the loop ($r = foreach (...) { ... }) or use List[T].
  • The foreach statement and the .Where() / .ForEach() methods are much faster than ForEach-Object and Where-Object for data already in memory; the cmdlets shine for streaming.
  • Filter left with cmdlet parameters, and discard output with $null = rather than | Out-Null in hot loops.
  • Build strings with -join or a StringBuilder, not repeated +=.

Always measure with Measure-Command { ... } before and after an optimisation.

Example

$count = 0
function Bump { $count++; "inside: $count" }        # creates a LOCAL copy
Bump; "outside: $count"                             # inside: 1 / outside: 0
function BumpScript { $script:count++ }
BumpScript; "outside: $count"                       # outside: 1
$n = 20000
$slow = Measure-Command { $a = @(); foreach ($i in 1..$n) { $a += $i } }
$fast = Measure-Command { $a = foreach ($i in 1..$n) { $i } }
$slow.TotalMilliseconds -gt $fast.TotalMilliseconds * 10   # True
$cmdlet = Measure-Command { 1..$n | ForEach-Object { $_ * 2 } }
$stmt   = Measure-Command { foreach ($i in 1..$n) { $i * 2 } }
$cmdlet.TotalMilliseconds -gt $stmt.TotalMilliseconds      # True
(1..$n).Where({ $_ % 2 -eq 0 }, 'First', 3) -join ','      # 2,4,6  (stops early)

Remember

  • Assignment creates a local variable; use $script: or $global: deliberately
  • Capture loop output instead of +=; prefer foreach and .Where() in memory
  • Measure-Command before and after optimising

Watch out for

  • Expecting a function's $total += 1 to update the script's $total.

Check yourself

  1. A script sets $n = 1; a function runs `$n = 5`. What is $n at script level afterwards?
  2. Which builds a 100,000-item result fastest?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_scopes

Builds on

Unlocks

Nothing builds on this yet.