Visitar URL original
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

  1. AkshayDhola commented on Sep 4, 2026

    @AkshayDhola
    Author

    I have a patch for this and would like to submit it — could I get an assignment?
    @powershell/powershell

    What's ready:

    • A one-hunk change in SwitchOps.ConditionSatisfiedRegex routing the string-pattern
      branch through ParserOps.NewRegex, the same cache -match, -replace and -split
      already use.
    • A new test file, test/powershell/Language/Scripting/SwitchRegex.Tests.ps1, 12 cases.
      There is currently no coverage for switch -regex anywhere under test/powershell, so
      this is new coverage regardless of whether the fix lands. It passes on both the patched
      and unpatched binary, since it asserts behavior rather than speed.
    • test/powershell/Language and test/powershell/engine (6764 tests) show no regressions
      against an unpatched baseline build of the same commit.

    All three binaries ran the same script back to back in one session, median of 5:

    Binary Scenario A Scenario B Allocated (A)
    7.6.5, installed release 1690.4 ms 22560.2 ms 1008.6 MB
    7.7.0-preview.4 Release, unpatched 1144.0 ms 21764.1 ms 1008.6 MB
    7.7.0-preview.4 Release, patched 305.3 ms 858.8 ms 250.3 MB

    Only the last two rows are a like-for-like speedup comparison — 3.7x on scenario A and
    25.3x on scenario B, with allocation down 75%. The 7.6.5 row is there to show the defect
    exists on a shipped build; its absolute timings are not comparable, because the released
    package is ReadyToRun-compiled and a local Start-PSBuild is not. Allocation is comparable,
    and it is identical at 1008.6 MB on both unpatched builds.

    One design point I would rather raise up front than have surface in review: routing through
    ParserOps.NewRegex means clause patterns are retained in a process-wide cache of up to 1000
    entries instead of the static cache's 15, and [regex]::CacheSize no longer influences
    switch -regex. That makes switch consistent with the other regex operators, but it is a
    policy change — let me know if you'd prefer a different approach.

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