Repository navigation
Speed up parameter binding by skipping mandatory parameters outside the active parameter set - #28371
Blake (blakedeckard) wants to merge 1 commit into
Conversation
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 <noreply@anthropic.com>
|
Azure Pipelines: There may be pipelines that require an authorized user to comment /azp run to run. |
|
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 |
There was a problem hiding this comment.
🟢 Approval recommended
No active review issues remain for the parameter-set membership guard.
No open issues 🎉
What changed in this PR
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:
- Adds an early exit before enumerating unrelated parameter-set metadata.
- Retains checks for parameters that belong to all parameter sets.
| File | Description |
|---|---|
src/System.Management.Automation/engine/CmdletParameterBinderController.cs |
Adds a parameter-set membership guard in GetMissingMandatoryParameters. |
Review effort: Balanced
|
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
} |
|
@microsoft-github-policy-service agree |
PR Summary
Skip mandatory-parameter bookkeeping for parameters that are not in any of the currently valid parameter sets.
For every pipeline object,
CmdletParameterBinderController.GetMissingMandatoryParametersloops over each unbound parameter that is mandatory in some parameter set and enumerates its parameter set data, even when the parameter is not in any currently valid set. For commands with many parameter sets this is a large part of the per-object binding cost.Where-Objecthas 32 parameter sets (one per operator switch) and 32 parameters that are mandatory in one of them, so with the script block parameter set all 31 enumerations find nothing for every input object.The new check is the same one
BindUnboundParametersForBindingStateInParameterSet(and several other places) already use:(ParameterSetFlags & _currentParameterSetFlag) == 0 && !IsInAllSets. SinceParameterSetFlagsis the OR of all the parameter's set bits andIsInAllSetsis set when any of its sets is__AllParameterSets(GenerateParameterSetMappingFromMetadata),GetMatchingParameterSetDatareturns nothing for such a parameter, so the result is unchanged.PR Context
Measured on Linux x64, Release build with ReadyToRun. Each number is the median of 5 alternating process runs; each run takes the fastest of 5 repetitions:
1..200000 | Where-Object { $_ % 2 }$objs | Where-Object Tag -eq 3(50k objects)1..200000 | ForEach-Object { $_ }Per object, measured in-process with
[GC]::GetTotalAllocatedBytesand[Diagnostics.Stopwatch]:1..200000 | Where-Object { $_ % 2 }$objs | Where-Object Tag -eq 3(50k objects)1..200000 | ForEach-Object { $_ }Allocated bytes were identical in every run.
Benchmark script
Run: ./bench-where-object.ps1 -Before /pwsh -After /pwsh
Testing
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.
PR Checklist
.h,.cpp,.cs,.ps1and.psm1files have the correct copyright header