Skip to content

Improve Select and Retrieve performance#14

Open
devin-ai-integration[bot] wants to merge 3 commits intomasterfrom
devin/1770917208-select-retrieve-perf
Open

Improve Select and Retrieve performance#14
devin-ai-integration[bot] wants to merge 3 commits intomasterfrom
devin/1770917208-select-retrieve-perf

Conversation

@devin-ai-integration
Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration bot commented Feb 12, 2026

Summary

Reduces per-call and per-row allocations and overhead in the Select/Retrieve hot paths.

Allocation & overhead reductions (initial pass)

  • Fix Hash.Compute thread safety: Replaced static mutable fields (_a, _b, _c) with local variables passed by ref. The old implementation was not thread-safe — concurrent callers would corrupt shared state, producing incorrect hash values and cache misses.
  • new Param[] {}Array.Empty<Param>(): Avoids allocating a throwaway empty array on every cached Retrieve call (both sync and async paths).
  • GuessOperationType: indexed for loop instead of operation.Any(char.IsWhiteSpace): Avoids delegate allocation per call.
  • GetQueryKey optimization: Changed parameter from IEnumerable<Param> to IList<Param>, replaced LINQ chain (OrderBy/Select/ToDelimitedString) with a manual StringBuilder loop, and skips sorting when there's only one parameter.
  • Trimmed redundant Log.Capture calls in cache hit/miss paths to reduce string interpolation overhead.

Deeper per-row optimizations (latest revision)

  • Bypass Map dispatch in ConvertDataReader: The generic Map<TSource, TResult> method performs MappingFactory.IsIndexer (3 type checks) + additional is pattern matching on every row, even though the source is always IDataRecord. Now calls FastIndexerMapper<IDataRecord, T>.Map / FastIndexerMapperWithTypeCoercion<IDataRecord, T>.Map directly.
  • Pre-cache Mapper.PropertyMapper delegates for multi-map secondary types: Avoids per-row Tuple<Type,Type,bool,bool> allocation + ConcurrentDictionary.GetOrAdd lookup in Mapper.CreateDelegate for each secondary type.
  • Pre-resolve mapper delegate per result set in ConvertDataReaderMultiResult: Same optimization for multi-result queries.
  • Cache primary key column names per type: New PrimaryKeyColumnCache avoids per-row LINQ (Where/Select/OrderBy/ToArray) in CreateIdentity.
  • Pre-allocate args array before per-row loop: Avoids new object[types.Count] allocation on every row in multi-map scenarios.
  • Cache config.AutoTypeCoercion and types[resultIndex] in locals: Avoids repeated property access and list indexer lookups per row.
  • Replace LINQ ToList() with array allocation for reflectedTypes in multi-result path.
  • Pre-size HashSet with FieldCount capacity in GetColumns (netstandard2.1+/net8.0 only due to API availability).

All 230 unit tests pass.

Review & Testing Checklist for Human

  • ⚠️ Verify FastIndexerMapper dispatch is correct — The code now bypasses Map<TSource, TResult> and calls FastIndexerMapper<IDataRecord, T>.Map directly. Verify this is semantically equivalent for all entity types (interfaces, concrete classes, with/without type coercion).
  • ⚠️ Verify secondary mapper pre-caching — For multi-map queries, Mapper.CreateDelegate(typeof(IDataRecord), types[i], true, autoTypeCoercion) is called once before the loop. The original code went through Map(object, Type, bool) which did IsIndexer + GetIndexerType. Confirm the pre-cached delegate produces identical results.
  • Verify Log.CaptureEnd balance — The diff removes Log.CaptureBegin calls but keeps some Log.CaptureEnd calls on the cache-hit return paths. This may cause mismatched begin/end in the logging stack.
  • Confirm cache key format change is acceptableGetQueryKey output format changed slightly (no trailing "/" when params are empty). Existing in-memory L1 cache keys will no longer match after upgrade, causing a one-time cache miss on first call.
  • Run the Nemo.Benchmark project against a database to measure actual throughput improvement on Select/Retrieve paths (e.g., dotnet run -c Release in tests/Nemo.Benchmark/).

Notes

- Hoist WrappedRecord allocation out of per-row loop in ConvertDataReader
- Hoist WrappedReader allocation out of per-row loop in ConvertDataReaderMultiResult
- Fix Hash.Compute thread safety: replace static mutable fields with local variables
- Replace new Param[] {} with Array.Empty<Param>() to avoid allocations
- Optimize GuessOperationType to use indexed loop instead of LINQ Any()
- Optimize GetQueryKey: reduce LINQ overhead with manual StringBuilder iteration
- Remove redundant Log.Capture calls in cache hit/miss paths

Co-Authored-By: Max Stepanskiy <mstepanskiy@asicentral.com>
@devin-ai-integration
Copy link
Copy Markdown
Contributor Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment and CI monitoring

devin-ai-integration bot and others added 2 commits February 12, 2026 17:46
…d cannot be shared across rows

Co-Authored-By: Max Stepanskiy <mstepanskiy@asicentral.com>
- Cache primary key column names per type to avoid per-row LINQ in CreateIdentity
- Pre-allocate args array and cache map/typeCount before per-row loop
- Cache config.AutoTypeCoercion before loops to avoid repeated property access
- Bypass Map dispatch in ConvertDataReader: call FastIndexerMapper directly, avoiding per-row IsIndexer type checks and null checks
- Pre-cache Mapper delegates for secondary types in multi-map queries, avoiding per-row Tuple allocation and ConcurrentDictionary lookup
- Pre-resolve Mapper delegate per result set in ConvertDataReaderMultiResult
- Cache types[resultIndex] in local variable to avoid repeated list indexer access
- Replace LINQ ToList with array allocation for reflectedTypes in multi-result path
- Pre-size HashSet with FieldCount capacity in GetColumns (netstandard2.1+/net8.0)

Co-Authored-By: Max Stepanskiy <mstepanskiy@asicentral.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants