[perf-improver] perf: eliminate intermediate dictionary and generator allocations in ValueSet constructor#469
Conversation
…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>
Greptile SummaryThis PR optimizes the
Confidence Score: 5/5Safe 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
Reviews (1): Last reviewed commit: "perf: eliminate intermediate dictionary ..." | Re-trigger Greptile |
🤖 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.FromObjectornew ValueSet(id, category, itemType, IDictionary<string,object>)goes through this constructor.Change
File:
src/Examine.Core/ValueSet.csThe
IDictionary<string, object>constructor previously took two hops to reach the private canonical constructor:The new code goes in a single step:
The now-unused private
Yield()iterator helper is also removed.Performance Evidence
Methodology: Allocation path analysis. For an item with N fields:
Dictionary<string, IEnumerable<object>>Yieldstate machine objectsList<object>with 4-slotobject[4]backing (for 1 value each)object[1]arrays (exact size)For a typical item with 10 fields: 21 fewer heap allocations per
ValueSetconstruction fromIDictionary<string,object>(1 intermediate dict + 10 generators + 10 over-allocated lists).ValueSet.FromObject(the most common entry point) always routes through this constructor, so everyFromObjectcall benefits.Trade-offs
object[]instead ofList<object>. Both implementIReadOnlyList<object>, so the public API is unchanged.List<object>(e.g. via reflection or casting) would break, butValuesis typed asIReadOnlyDictionary<string, IReadOnlyList<object>>— no legitimate caller should depend on the concrete type.Reproducibility
Test Status
✅ Build succeeded (0 errors, 0 new warnings from project code).
✅ All 147 tests passed, 2 skipped (same baseline).
Add this agentic workflows to your repo
To install this agentic workflow, run