switch -regex recompiles the clause pattern on every successful match · Issue #27975 · PowerShell/PowerShell · GitHub
Skip to content

switch -regex recompiles the clause pattern on every successful match #27975

Description

@AkshayDhola

Prerequisites

Steps to reproduce

switch -regex builds a new Regex for every input object that matches, instead of reusing a
cached one. The script below is self-contained and prints the numbers quoted under
Actual behavior.

# 1. Build a 200,000-line log.
$log = Join-Path ([IO.Path]::GetTempPath()) 'switch-regex-bench.log'
if (-not (Test-Path $log)) {
    $sb = [System.Text.StringBuilder]::new()
    for ($i = 1; $i -le 200000; $i++) {
        $null = $sb.AppendLine("2026-09-04T00:00:0$($i % 10) ERROR thing $i failed")
    }
    Set-Content -Path $log -Value $sb.ToString() -NoNewline
}

function Measure-Median ([scriptblock] $Action, [int] $Count = 5) {
    $times = foreach ($i in 1..$Count) { (Measure-Command $Action).TotalMilliseconds }
    ($times | Sort-Object)[[int]($Count / 2)]
}

# 2. Scenario A: one clause, every line matches.
$a = Measure-Median {
    switch -regex -file $log {
        '^(?<ts>\S+) ERROR (?<msg>.*)$' { $null = $matches }
    }
}

# 3. Scenario B: 23 non-matching clauses ahead of the matching one, so 24 distinct
#    patterns are evaluated per line -- more than [regex]::CacheSize, which is 15.
$clauses = (1..23 | ForEach-Object { "'NOMATCH{0:d2}(?<a>x)' {{ continue }}" -f $_ }) -join "`n    "
$b = Measure-Median ([scriptblock]::Create(@"
switch -regex -file `$log {
    $clauses
    '^(?<ts>\S+) ERROR (?<msg>.*)$' { `$null = `$matches }
}
"@))

# 4. Allocation cost of scenario A.
[GC]::Collect(); [GC]::WaitForPendingFinalizers(); [GC]::Collect()
$before = [GC]::GetTotalAllocatedBytes($true)
switch -regex -file $log { '^(?<ts>\S+) ERROR (?<msg>.*)$' { $null = $matches } }
$mb = ([GC]::GetTotalAllocatedBytes($true) - $before) / 1MB

'Scenario A : {0:N1} ms  (1 clause, median of 5)'   -f $a
'Scenario B : {0:N1} ms  (24 clauses, median of 5)' -f $b
'Ratio B/A  : {0:N1}x'                             -f ($b / $a)
'Allocated  : {0:N1} MB (scenario A)'               -f $mb

Expected behavior

Each distinct clause pattern compiles once and is reused, the way -match, -replace and -split
already behave. Adding clauses costs one extra match attempt per line, not a recompile, so
scenario B stays a small multiple of scenario A.

Actual behavior

PowerShell 7.6.5, 200000 lines, median of 5 runs:

  Scenario A (1 clause)    :   1690.4 ms
  Scenario B (24 clauses)  :  22560.2 ms      13.3x, from clause count alone
  Allocated, scenario A    :   1008.6 MB      about 5 KB per matched line

Every matching line constructs a fresh Regex. Once the clause count passes Regex.CacheSize (15),
the static cache thrashes and every clause recompiles for every line.

Error details

Environment data

Name                           Value
----                           -----
PSVersion                      7.6.5
PSEdition                      Core
GitCommitId                    7.6.5
OS                             Arch Linux
Platform                       Unix
PSCompatibleVersions           {1.0, 2.0, 3.0, 4.0…}
PSRemotingProtocolVersion      2.4
SerializationVersion           1.1.0.1
WSManStackVersion              3.0

Visuals

No response

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Needs-TriageThe issue is new and needs to be triaged by a work group.

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions