Skip to content

Improve GetChildren / GetChildKeys performance for large configurations#130306

Draft
rosebyte wants to merge 1 commit into
dotnet:mainfrom
rosebyte:experiment/getchildkeys
Draft

Improve GetChildren / GetChildKeys performance for large configurations#130306
rosebyte wants to merge 1 commit into
dotnet:mainfrom
rosebyte:experiment/getchildkeys

Conversation

@rosebyte

@rosebyte rosebyte commented Jul 7, 2026

Copy link
Copy Markdown
Member

Fixes #65885.

Measured with a small custom harness (minimum of 15 timed runs; GC.GetAllocatedBytesForCurrentThread for allocation) on .NET 11 preview, Apple arm64. Absolute numbers are machine-specific and the modern binder is far faster than the .NET 6 the issue was filed against, so the ratios are the point. A BenchmarkDotNet re-run on the CI baseline is advisable before merge.

The issue's repro shape (single JSON-equivalent provider, 6003 keys, binding a small object from MySection:Example):

Operation main this PR speed-up main alloc this PR alloc
GetChildren("MySection:Example") (2 children) ~25 000 ns ~6 600 ns 3.8x 1 064 B 928 B
GetChildren("OtherSection") (2 children, 6000 descendants) ~886 000 ns ~65 000 ns 13.6x 324 272 B 896 B
ConfigurationBinder.Get<ConfiguredObject> (end to end) ~28 000 ns ~18 300 ns 1.5x ~1 900 B ~1 980 B

The end-to-end bind is "only" 1.5x because on .NET 11 the binder's own reflection cost now dominates the greatly reduced aggregation; the part this PR changes is 3.8x-13.6x faster and allocates up to 99.7% less.

Provider-count scaling (one GetChildren, 100 distinct keys carried by P overlapping providers):

P main this PR
8 465 us 15 us
16 1.76 ms 25 us
32 7.04 ms 45 us
64 29.1 ms 84 us

main grows as O(P^2) (about 4x per doubling); this PR grows as O(P) (about 1.9x per doubling): 347x at 64 providers.

Small in-memory configurations (typical case, time and bytes per GetChildren):

Scenario main this PR alloc main -> PR
1 child 261 ns 114 ns 1 328 -> 704 B (-47%)
100 distinct over 6 providers 266 us 12 us 72 072 -> 25 640 B (-64%)
leaf, 0 children 239 ns 84 ns 640 -> 216 B (-66%)

What this changes, and what it does not

  • Fixed: the provider aggregation (O(P^2) -> O(P)), the LINQ and per-provider allocations, and the materialise-then-de-duplicate behaviour. This is the cost the issue reports.
  • Not changed: a single GetChildren still scans the whole provider's Data (O(K) per call); it is now much leaner, but not sub-linear. Making it sub-linear needs a per-provider sorted key index (binary-search the parentPath: range) so a call becomes O(log K + children). That was prototyped and deliberately left out: it is a parallel structure with invalidation and retained memory whose build cost only amortises after roughly log K calls, so it is a poor trade for the common "read each section once or twice" usage and was not needed to resolve the reported cost. It remains an option for a follow-up if a workload does thousands of GetChildren calls against one huge provider.

Compatibility and risk

  • Public API surface is unchanged.
  • Ordering and de-duplication semantics are preserved (case-insensitive de-duplication, empty segment first, integer segments before text and ordered numerically).
  • Custom IConfigurationProvider implementations that override GetChildKeys are unaffected: a foreign returned sequence is absorbed by the accumulator.

Copilot AI review requested due to automatic review settings July 7, 2026 15:10
@rosebyte rosebyte changed the title Improve child keys aggregation Improve GetChildren / GetChildKeys performance for large configurations Jul 7, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR refactors how configuration child keys are aggregated across providers, aiming to reduce allocations and improve correctness/consistency by de-duplicating keys during aggregation (rather than at the end), while keeping ordering via ConfigurationKeyComparer. It also adds targeted tests around de-duplication, large key sets, deep keys, and provider key-filtering behavior.

Changes:

  • Introduces SortedChildKeys as an internal accumulator to de-duplicate and lazily sort child keys.
  • Updates GetChildrenImplementation (and chained-provider child key retrieval) to use the new accumulator-based aggregation.
  • Adds tests validating de-duplication and various edge cases for GetChildren() / GetChildKeys() behavior.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
src/libraries/Microsoft.Extensions.Configuration/tests/ConfigurationTest.cs Adds test coverage for de-duplication and edge cases around child key aggregation.
src/libraries/Microsoft.Extensions.Configuration/src/SortedChildKeys.cs New internal accumulator type for de-duplicating + lazily sorting child keys.
src/libraries/Microsoft.Extensions.Configuration/src/InternalConfigurationRootExtensions.cs Reworks provider aggregation and section projection to use SortedChildKeys.
src/libraries/Microsoft.Extensions.Configuration/src/ConfigurationProvider.cs Updates base provider to add keys directly into SortedChildKeys when used as the seed.
src/libraries/Microsoft.Extensions.Configuration/src/ConfigurationKeyComparer.cs Avoids unnecessary parsing work / allocations by gating TryParse attempts.
src/libraries/Microsoft.Extensions.Configuration/src/ChainedConfigurationProvider.cs Uses the accumulator-based path to avoid per-child section allocations when chaining.

Comment on lines +107 to 114
private static void ProcessProvider(IConfigurationProvider provider, SortedChildKeys accumulator, string? path)
{
IEnumerable<string> returned = provider.GetChildKeys(accumulator, path);
if (!ReferenceEquals(returned, accumulator))
{
accumulator.Overwrite(returned);
}
}
Comment on lines +150 to +158
public ChildKeysBag(SortedChildKeys accumulator)
{
_accumulator = accumulator;
}

public ChildKeysBag(List<string> fallback)
{
_fallback = fallback;
}
Comment on lines +95 to +99
SortedChildKeys accumulator = earlierKeys is SortedChildKeys existing ? existing : new(earlierKeys);
if (_config is IConfigurationRoot root)
{
return root.GetChildKeysImplementation(parentPath, accumulator);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

A large set of configuration keys provided by the built-in .NET IConfigurationProvider's negatively affect performance significantly

2 participants