Skip to content

Avoid HashSet allocations for singleton conditional dependencies - #132934

Open
awakecoding wants to merge 1 commit into
dotnet:mainfrom
awakecoding:copilot/nativeaot-conditional-dependency-buckets
Open

Avoid HashSet allocations for singleton conditional dependencies#132934
awakecoding wants to merge 1 commit into
dotnet:mainfrom
awakecoding:copilot/nativeaot-conditional-dependency-buckets

Conversation

@awakecoding

Copy link
Copy Markdown

Summary

  • Store the first pending conditional dependency directly in a small reference-backed bucket.
  • Promote the bucket to a HashSet only when a second distinct dependency is added.
  • Preserve the existing condition-key equality, dependency equality/deduplication, and replay behavior.

Motivation

DependencyAnalyzer currently allocates a HashSet<CombinedDependencyListEntry> for every
condition that has a pending conditional dependency, even when that condition has only one
dependency.

In a full Remote Desktop Manager NativeAOT compilation using the matching .NET 10.0.11
toolchain, the analyzer created 5,724,010 conditional buckets, but only 1,189,313 (20.78%)
needed more than one distinct entry. A reference-backed singleton representation reduced
managed allocation by 0.66-0.71 GiB in both measured runs. An earlier value-type prototype
was rejected because copying the large dictionary value erased the timing benefit; keeping
the bucket behind a reference avoids those dictionary-value copies.

Implementation

The dictionary key and its equality behavior are unchanged. Each dictionary value is now a
small ConditionalDependencyBucket object:

  • The first CombinedDependencyListEntry is stored directly.
  • A duplicate of that entry is ignored with the same equality contract used by the old
    HashSet.
  • A second distinct entry creates a HashSet containing both entries.
  • Further insertions go directly to the promoted set, without additional wrappers.
  • Satisfying a condition removes its bucket from the dictionary before replaying the stored
    dependencies.

The change does not alter conditional dependency production, graph sorting, profiling,
capacity policy, or any other NativeAOT optimization.

Validation

  • ILCompiler.Compiler.Tests: 33/33 passed in Release.
  • ILCompiler.Compiler.Tests: 33/33 passed in Debug.
  • Release NativeAOT smoke tree: all 43 projects built.
  • Release NativeAOT smoke execution: 28/28 passed.
  • NativeAOT determinism test: both 11,021,593-byte object files had SHA-256
    B09CDFB966C0306D667EEA682A54D8D71816ACB6CA2F5487952D66B65877CE02.

Focused analyzer tests cover singleton and promoted buckets, duplicate entries before and
after promotion, distinct owner nodes, condition-first and dependency-first marking,
already-marked dependencies, null conditions, deferred dependency computation, replay and
removal, condition identity, and distinct entries across promotion. The NativeAOT determinism
test and current-main A/B output hashes cover ordering and output stability.

Current-main stress benchmark

The benchmark used current main at c210d82dbc1ab432b9369604a1caef9a0ab763d2.
It constructed 2,000,000 pending conditional buckets and promoted 415,552 (20.7776%), matching
the full-RDM promotion ratio. Owners were marked before a trigger marked every condition.
Baseline and changed analyzer assemblies were run in alternating order for 12 measured pairs
after one warmup per variant.

Metric (median of 12 runs) Baseline Changed Delta
Graph wall time 4,137.22 ms 3,762.04 ms -9.07%
Mark time 4,113.62 ms 3,728.33 ms -9.37%
Process CPU 7,218.75 ms 6,429.69 ms -10.93%
Managed allocation 1,286.96 MiB 1,040.04 MiB -246.92 MiB (-19.19%)
Peak private memory 1,628.80 MiB 1,305.68 MiB -323.12 MiB (-19.84%)
Peak working set 1,549.35 MiB 1,245.26 MiB -304.09 MiB (-19.63%)
Gen0 / Gen1 / Gen2 collections 4 / 2 / 2 5 / 3 / 3 varies with GC timing

Every measured run produced the same 25,662,216-byte logical marked-node output with
SHA-256 03357BB1EB8FE3A673546DA58681BD6EB651E5799330BD1908508B02BEECB9D5.

Managed allocation fell by 246.91-246.93 MiB in every pair. Timing was much noisier:
individual paired graph deltas ranged from -38.26% to +63.35%. The median timing result is
directionally favorable, but the repeatable allocation reduction and output identity are
the primary current-main evidence.

Retained full-application evidence

The separate .NET 10.0.11 RDM experiment observed:

  • 5,724,010 buckets and 1,189,313 promotions.
  • 0.66-0.71 GiB lower managed allocation in both runs.
  • Byte-identical 3,744,339,247-byte object output with SHA-256
    C71AA7E9BAC37D9F2A2B2E2D75C695947D70CBD4EE39464C3623C4C9A8B75668.
  • A frequency-matched confirmation reduced graph time by 6.76 seconds, mark time by
    9.12 seconds, and total time by 9.98 seconds.

The first RDM timing run was frequency-confounded, and peak private memory varied with GC
timing (the confirmation was higher). Those results are retained as supporting historical
evidence rather than treated as current-main timing proof.

Note

This pull request description was drafted with GitHub Copilot.

Store the first pending conditional dependency directly in a small reference-backed bucket. Promote to a HashSet only when a second distinct dependency is added, avoiding per-bucket HashSet storage for the common singleton case.
Copilot AI lite review requested due to automatic review settings August 29, 2026 23:10
@dotnet-policy-service dotnet-policy-service Bot added the community-contribution Indicates that the PR has been added by a community member label Aug 29, 2026
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 3 pipeline(s).
13 pipeline(s) were filtered out due to trigger conditions.
There may be pipelines that require an authorized user to comment /azp run to run.

@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @agocke, @dotnet/ilc-contrib
See info in area-owners.md if you want to be subscribed.

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.

🟢 Approval recommended

Pull request overview

This PR optimizes NativeAOT’s dependency analysis by avoiding a HashSet<CombinedDependencyListEntry> allocation for the common case where a condition only accumulates a single pending conditional dependency, while preserving existing deduplication and replay semantics.

Changes:

  • Replaces the conditional-dependency dictionary value from HashSet<CombinedDependencyListEntry> to a small reference-backed ConditionalDependencyBucket that stores the first entry inline and promotes to HashSet on the second distinct add.
  • Uses Dictionary.Remove(key, out value) to remove-and-replay stored conditional dependencies in one lookup when a condition node becomes marked.
  • Adds focused unit tests covering singleton vs promoted buckets, deduplication (including across promotion), owner/condition identity semantics, deferred dependency computation, and replay behavior.
File summaries
File Description
src/coreclr/tools/aot/ILCompiler.DependencyAnalysisFramework/DependencyAnalyzer.cs Introduces a singleton-then-promote bucket to reduce per-condition allocations and replays stored dependencies when conditions are satisfied.
src/coreclr/tools/aot/ILCompiler.Compiler.Tests/DependencyGraphTests.cs Adds regression tests validating conditional dependency storage, deduplication, replay, and ordering/identity invariants.
Review details
  • Files reviewed: 2/2 changed files
  • Comments generated: 0
  • Review effort level: Lite

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

Labels

area-NativeAOT-coreclr community-contribution Indicates that the PR has been added by a community member

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants