Skip to content

[perf-improver] perf: eliminate intermediate dictionary and generator allocations in ValueSet constructor#469

Open
github-actions[bot] wants to merge 1 commit into
support/3.xfrom
perf-assist/valueset-reduce-allocs-1f52c47-4a724a109fe6820a
Open

[perf-improver] perf: eliminate intermediate dictionary and generator allocations in ValueSet constructor#469
github-actions[bot] wants to merge 1 commit into
support/3.xfrom
perf-assist/valueset-reduce-allocs-1f52c47-4a724a109fe6820a

Conversation

@github-actions

@github-actions github-actions Bot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

🤖 This is an automated PR from Perf Improver, an AI assistant focused on performance.

Goal

Reduce heap allocations on the indexing hot path — every item indexed via ValueSet.FromObject or new ValueSet(id, category, itemType, IDictionary<string,object>) goes through this constructor.

Change

File: src/Examine.Core/ValueSet.cs

The IDictionary<string, object> constructor previously took two hops to reach the private canonical constructor:

IDictionary<string,object>
  → values.ToDictionary(key, Yield(value))          ← intermediate dict + N generator state machines
  → IDictionary<string,IEnumerable<object>>
  → values.ToDictionary(key, value.ToList())         ← N List<object> with 4-slot backing arrays
  → IReadOnlyDictionary<string,IReadOnlyList<object>>  ← final form

The new code goes in a single step:

// Before
public ValueSet(string id, string category, string itemType, IDictionary<string, object> values)
    : this(id, category, itemType, values.ToDictionary(x => x.Key, x => Yield(x.Value)))

// After
public ValueSet(string id, string category, string itemType, IDictionary<string, object> values)
    : this(id, category, itemType,
        (IReadOnlyDictionary<string, IReadOnlyList<object>>)values.ToDictionary(
            x => x.Key,
            x => (IReadOnlyList<object>)new[] { x.Value }))

The now-unused private Yield() iterator helper is also removed.

Performance Evidence

Methodology: Allocation path analysis. For an item with N fields:

Site Before After
Intermediate dictionary 1 Dictionary<string, IEnumerable<object>> ✅ eliminated
Per-field generator N compiler-generated Yield state machine objects ✅ eliminated
Per-field list N List<object> with 4-slot object[4] backing (for 1 value each) ✅ eliminated
Per-field storage N object[1] arrays (exact size)

For a typical item with 10 fields: 21 fewer heap allocations per ValueSet construction from IDictionary<string,object> (1 intermediate dict + 10 generators + 10 over-allocated lists).

ValueSet.FromObject (the most common entry point) always routes through this constructor, so every FromObject call benefits.

Trade-offs

  • Values are now backed by object[] instead of List<object>. Both implement IReadOnlyList<object>, so the public API is unchanged.
  • Callers who previously relied on the runtime type being List<object> (e.g. via reflection or casting) would break, but Values is typed as IReadOnlyDictionary<string, IReadOnlyList<object>> — no legitimate caller should depend on the concrete type.

Reproducibility

dotnet build src/Examine.sln --configuration Release
dotnet test src/Examine.Test/Examine.Test.csproj --configuration Release --filter "TestCategory!=Benchmarks" -f net8.0

Test Status

✅ Build succeeded (0 errors, 0 new warnings from project code).
✅ All 147 tests passed, 2 skipped (same baseline).

Generated by Perf Improver · sonnet46 5.3M ·
Comment /perf-assist to run again

Add this agentic workflows to your repo

To install this agentic workflow, run

gh aw add githubnext/agentics/workflows/perf-improver.md@dcdf09723d42ef9b6c75335e4612fd145d4ccdaa

…ValueSet(IDictionary<string,object>) constructor

Replace the two-step conversion (IDictionary<string,object> → Yield() generator
→ IDictionary<string,IEnumerable<object>> → .ToList() → final dict) with a direct
single-step construction using new[]{x.Value}.

Before (per indexed item with N fields):
  - 1 intermediate Dictionary<string, IEnumerable<object>>
  - N compiler-generated Yield() state machine objects
  - N List<object> instances with 4-slot backing array (for just 1 value each)

After:
  - N object[1] arrays (exact size, no over-allocation)
  - 0 intermediate dictionaries, 0 generator objects

Also removes the now-dead private Yield() helper method.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@Shazwazza Shazwazza marked this pull request as ready for review June 10, 2026 13:38
@greptile-apps

greptile-apps Bot commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR optimizes the ValueSet(string, string, string, IDictionary<string, object>) constructor by bypassing the intermediate IDictionary<string, IEnumerable<object>> overload and instead building the final Dictionary<string, IReadOnlyList<object>> in a single pass, replacing List<object> per-field allocations with exact-size object[1] arrays and eliminating the now-unused Yield iterator helper.

  • Allocation reduction: For a document with N fields, the path previously created 1 intermediate dictionary, N generator state machines, and N over-allocated List<object> (4-slot backing arrays); the new path creates only N object[1] arrays.
  • API compatibility: Values is typed IReadOnlyDictionary<string, IReadOnlyList<object>>; swapping List<object> for object[] is invisible to all callers using the declared interface type.

Confidence Score: 5/5

Safe to merge — the change is a straightforward single-file allocation optimization with no behavioral impact on the public API.

The new constructor body produces a Dictionary<string, IReadOnlyList> and casts it to IReadOnlyDictionary<string, IReadOnlyList>, which is always valid because Dictionary<TKey,TValue> has implemented that interface since .NET 4.5. Each object[1] array correctly implements IReadOnlyList (arrays implement IReadOnlyList since .NET 4.5 as well). The Yield removal is safe — nothing else called it. The only observable difference is that Values entries are now backed by object[] instead of List, but Values is typed as IReadOnlyDictionary<string, IReadOnlyList>, so no legitimate caller can distinguish the two. The Clone path is unaffected.

No files require special attention.

Important Files Changed

Filename Overview
src/Examine.Core/ValueSet.cs Single-step dictionary construction replaces the two-hop path through the IEnumerable overload; Yield helper removed; cast is safe because Dictionary<TKey,TValue> implements IReadOnlyDictionary<TKey,TValue> since .NET 4.5.

Reviews (1): Last reviewed commit: "perf: eliminate intermediate dictionary ..." | Re-trigger Greptile

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.

0 participants