GeezerVisonThe World Wide Web, restored to 800 pixels of glory. | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| Home|About|Gaming cabinet|Archives|How it works|Starter sites | ||||||||||||||||||||||||||||||||||||||||||||||||||||
GeezerVison is showing github.com — Speed up parameter binding by skipping mandatory parameters outside the active parameter set by blakedeckard · Pull Request #28371 · PowerShell/PowerShell Conversation
|
| Command | Before | After | Change |
|---|---|---|---|
1..200000 | Where-Object { $_ % 2 } |
691 ms | 422 ms | -39% |
$objs | Where-Object Tag -eq 3 (50k objects) |
132 ms | 80 ms | -40% |
1..200000 | ForEach-Object { $_ } |
420 ms | 406 ms | -3% |
Per object, measured in-process with [GC]::GetTotalAllocatedBytes and [Diagnostics.Stopwatch]:
| Command | Allocated before | Allocated after | Change | Time before | Time after | Change |
|---|---|---|---|---|---|---|
1..200000 | Where-Object { $_ % 2 } |
4,464 B | 2,232 B | -50% | 3.45 µs | 2.11 µs | -39% |
$objs | Where-Object Tag -eq 3 (50k objects) |
3,144 B | 912 B | -71% | 2.65 µs | 1.59 µs | -40% |
1..200000 | ForEach-Object { $_ } |
1,936 B | 1,792 B | -7% | 2.10 µs | 2.03 µs | -3% |
Allocated bytes were identical in every run.
<# .SYNOPSIS Before/after benchmark for "Skip unrelated parameters when checking for missing mandatory parameters". .DESCRIPTION Runs the Before and After pwsh builds in alternating processes (to spread out machine noise), and in each process measures every case in-process with [Diagnostics.Stopwatch] and [GC]::GetTotalAllocatedBytes. Each process reports the best of $Reps repetitions; the script then prints the median across $Rounds processes as two markdown tables. .EXAMPLE ./bench-where-object.ps1 -Before ~/ps-master/release/pwsh -After ~/ps-patched/release/pwsh (Build each with: Start-PSBuild -Configuration Release -Output release) #> param( [Parameter(Mandatory)] [string] $Before, [Parameter(Mandatory)] [string] $After, [int] $Rounds = 5, [int] $Reps = 5 ) # Code that runs inside each child pwsh process. Prints one JSON line. $worker = @' param([int] $Reps) $objs = 1..50000 | ForEach-Object { [pscustomobject]@{ Id = $_; Name = "item$_"; Tag = ($_ % 10) } } $cases = [ordered]@{ '`1..200000 | Where-Object { $_ % 2 }`' = @{ N = 200000; S = { 1..200000 | Where-Object { $_ % 2 } | Out-Null } } '`$objs | Where-Object Tag -eq 3` (50k objects)' = @{ N = 50000; S = { $objs | Where-Object Tag -eq 3 | Out-Null } } '`1..200000 | ForEach-Object { $_ }`' = @{ N = 200000; S = { 1..200000 | ForEach-Object { $_ } | Out-Null } } } $out = [ordered]@{} foreach ($name in $cases.Keys) { $c = $cases[$name] & $c.S; & $c.S # warm up (JIT, caches) $bestMs = [double]::MaxValue; $bestBytes = [long]::MaxValue for ($i = 0; $i -lt $Reps; $i++) { [GC]::Collect(); [GC]::WaitForPendingFinalizers() $a0 = [GC]::GetTotalAllocatedBytes($true) $sw = [Diagnostics.Stopwatch]::StartNew() & $c.S $sw.Stop() $a1 = [GC]::GetTotalAllocatedBytes($true) $bestMs = [Math]::Min($bestMs, $sw.Elapsed.TotalMilliseconds) $bestBytes = [Math]::Min($bestBytes, $a1 - $a0) } $out[$name] = @{ ms = $bestMs; n = $c.N; bytes = $bestBytes } } $out | ConvertTo-Json -Compress -Depth 3 '@ $workerPath = Join-Path ([IO.Path]::GetTempPath()) "bench-where-object-worker.ps1" Set-Content -Path $workerPath -Value $worker $results = @{ Before = @(); After = @() } for ($r = 1; $r -le $Rounds; $r++) { foreach ($side in 'Before', 'After') { $exe = if ($side -eq 'Before') { $Before } else { $After } Write-Host "round $r/$Rounds $side" $json = & $exe -NoProfile -NonInteractive -File $workerPath -Reps $Reps | Select-Object -Last 1 $results[$side] += , ($json | ConvertFrom-Json -AsHashtable) } } function Median([double[]] $v) { $s = $v | Sort-Object; if ($s.Count % 2) { $s[[int][Math]::Floor($s.Count / 2)] } else { ($s[$s.Count / 2 - 1] + $s[$s.Count / 2]) / 2 } } function Pct($a, $b) { '{0:+0;-0;0}%' -f (100 * ($b - $a) / $a) } $names = $results.Before[0].Keys '' "Whole pipeline, median of $Rounds alternating process runs, each the best of $Reps repetitions:" '' '| Command | Before | After | Change |' '|---|---:|---:|---:|' foreach ($n in $names) { $b = Median ($results.Before | ForEach-Object { $_[$n].ms }); $a = Median ($results.After | ForEach-Object { $_[$n].ms }) '| {0} | {1:N0} ms | {2:N0} ms | {3} |' -f ($n -replace '\|', '\|'), $b, $a, (Pct $b $a) } '' 'Per object, measured in-process with `[GC]::GetTotalAllocatedBytes` and `[Diagnostics.Stopwatch]`:' '' '| Command | Allocated before | Allocated after | Change | Time before | Time after | Change |' '|---|---:|---:|---:|---:|---:|---:|' foreach ($n in $names) { $cnt = $results.Before[0][$n].n $bb = Median ($results.Before | ForEach-Object { $_[$n].bytes / $cnt }); $ab = Median ($results.After | ForEach-Object { $_[$n].bytes / $cnt }) $bt = Median ($results.Before | ForEach-Object { $_[$n].ms * 1000 / $cnt }); $at = Median ($results.After | ForEach-Object { $_[$n].ms * 1000 / $cnt }) $bAll = $results.Before | ForEach-Object { [math]::Round($_[$n].bytes / $cnt) } | Sort-Object -Unique $aAll = $results.After | ForEach-Object { [math]::Round($_[$n].bytes / $cnt) } | Sort-Object -Unique '| {0} | {1:N0} B | {2:N0} B | {3} | {4:N2} µs | {5:N2} µs | {6} |' -f ($n -replace '\|', '\|'), $bb, $ab, (Pct $bb $ab), $bt, $at, (Pct $bt $at) if ($bAll.Count -gt 1 -or $aAll.Count -gt 1) { Write-Host "note: allocation varied across runs for $n" } } Remove-Item $workerPath -ErrorAction SilentlyContinue
Run: ./bench-where-object.ps1 -Before /pwsh -After /pwsh
I have included a debug script below in the first comment to step through the changes. Pester results (engine/ParameterBinding, engine/Basic, Language, Microsoft.PowerShell.Core/Utility/Management) were identical before and after.
.h, .cpp, .cs, .ps1 and .psm1 files have the correct copyright header
Sorry, something went wrong.
There was an error while loading. Please reload this page.
For every pipeline object, GetMissingMandatoryParameters walks all unbound parameters that are mandatory in some parameter set and enumerates their parameter set data, even when the parameter is not in any of the currently valid parameter sets. For cmdlets with many parameter sets, such as Where-Object, this is most of the per-object binding cost. Skip a parameter when it is neither in the current parameter sets nor in all sets, using the same check that BindUnboundParametersForBindingStateInParameterSet already uses. GetMatchingParameterSetData would return nothing for such a parameter, so the result does not change. Co-Authored-By: Claude Opus 5.5 <[email protected]>
|
Azure Pipelines: There may be pipelines that require an authorized user to comment /azp run to run. |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
|
TLDR, GetMatchingParameterSetData checks set entries of one parameter to find any in an active set. Before we do this, we now do one AND of the parameter's set bitmask against the active set. If the result is zero and the parameter isn't in all sets, none of its entries match, and we can skip the call. Where-Object is notable here as it has 32 parameter sets and 31 mandatory parameters that don’t apply to the active set: each object goes from 31 iterator calls that find nothing to 31 ANDs. This check seems to be repeated several places, and it could probably be a refactor candidate after this PR. If Claude co-author is an issue, let me know |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
No active review issues remain for the parameter-set membership guard.
No open issues 🎉
This PR reduces per-object parameter-binding overhead by skipping mandatory-parameter checks for parameters outside the currently valid parameter sets, while preserving binding behavior.
Changes:
| File | Description |
|---|---|
src/System.Management.Automation/engine/CmdletParameterBinderController.cs |
Adds a parameter-set membership guard in GetMissingMandatoryParameters. |
Review effort: Balanced
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
|
Confirmed in debugger as well, can compare with the following debug points, demo script, and launch.json block. Let me know if there is any other way I can assist. Line numbers are for this PR's commit ( Every breakpoint and logpoint below, except A, uses the condition Debug lines and logpoints Demo script ( # Debugger walkthrough for "Skip unrelated parameters when checking for missing mandatory parameters". # Run it under the C# debugger. Each section takes a different path through the patched check. $objs = 1..3 | ForEach-Object { [pscustomobject]@{ N = $_ } } Write-Host '--- 1. Where-Object script-block form: active set = ScriptBlockSet (0x2), every unbound mandatory parameter is skipped' $objs | Where-Object { $_.N -gt 1 } | Out-Null Write-Host '--- 2. Where-Object property form with -GT: active set = GreaterThanSet (0x20), every unbound mandatory parameter is skipped' $objs | Where-Object N -GT 1 | Out-Null # A tiny two-set function so you can watch both outcomes of the check: "keep" (falls through to the # old lookup) and "skip". function Test-Sets { [CmdletBinding(DefaultParameterSetName = 'ByName')] param( [Parameter(Mandatory, ValueFromPipeline, ParameterSetName = 'ByName')] [string] $Name, [Parameter(Mandatory, ParameterSetName = 'ById')] [int] $Id, [Parameter(ParameterSetName = 'ByName')] [string] $Tag ) process { } } Write-Host '--- 3. Test-Sets: first check runs before any object (Name kept, Id skipped), per-object checks see only ByName (Id skipped)' 'a', 'b' | Test-Sets Write-Host 'done' launch.json entry {
"name": "Debug patch walkthrough (demo.ps1)",
"type": "coreclr",
"request": "launch",
"program": "${workspaceFolder}/debug/pwsh",
"args": ["-NoProfile", "-NoLogo", "-File", "${workspaceFolder}/debug-walkthrough/demo.ps1"],
"cwd": "${workspaceFolder}",
"console": "integratedTerminal",
"justMyCode": false,
"suppressJITOptimizations": true,
"requireExactSource": true
} |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
@microsoft-github-policy-service agree |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
At least 1 approving review is required to merge this pull request.
There was an error while loading. Please reload this page.
Successfully merging this pull request may close these issues.
There was an error while loading. Please reload this page.