From 222432ba518534942eb485fa03c650d1c0975c67 Mon Sep 17 00:00:00 2001
From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Thu, 12 Feb 2026 17:29:42 +0000
Subject: [PATCH 1/3] Improve Select and Retrieve performance
- 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() 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
---
src/Nemo/ObjectFactory.Retrieve.cs | 39 +++++++++---------
src/Nemo/ObjectFactory.cs | 14 +++++--
src/Nemo/Utilities/Hash.cs | 63 ++++++++++++++----------------
3 files changed, 60 insertions(+), 56 deletions(-)
diff --git a/src/Nemo/ObjectFactory.Retrieve.cs b/src/Nemo/ObjectFactory.Retrieve.cs
index ff51c24..66ab433 100644
--- a/src/Nemo/ObjectFactory.Retrieve.cs
+++ b/src/Nemo/ObjectFactory.Retrieve.cs
@@ -58,9 +58,7 @@ private static IEnumerable RetrieveImplemenation(string operat
{
config ??= ConfigurationFactory.Get();
- queryKey = GetQueryKey(operation, parameters ?? new Param[] { }, returnType);
-
- Log.CaptureBegin($"Retrieving from L1 cache: {queryKey}", config);
+ queryKey = GetQueryKey(operation, parameters ?? Array.Empty(), returnType);
if (returnType == OperationReturnType.MultiResult)
{
@@ -74,8 +72,6 @@ private static IEnumerable RetrieveImplemenation(string operat
if (result != null)
{
- Log.Capture($"Found in L1 cache: {queryKey}", config);
-
if (returnType == OperationReturnType.MultiResult)
{
((IMultiResult)result).Reset();
@@ -84,8 +80,6 @@ private static IEnumerable RetrieveImplemenation(string operat
Log.CaptureEnd(config);
return result;
}
- Log.Capture($"Not found in L1 cache: {queryKey}", config);
- Log.CaptureEnd(config);
}
result = RetrieveItems(operation, parameters, operationType, returnType, connectionName, connection, types, map, schema, config, identityMap);
@@ -153,12 +147,25 @@ private static IEnumerable RetrieveItems(string operation, IList pa
return result;
}
- private static string GetQueryKey(string operation, IEnumerable parameters, OperationReturnType returnType)
+ private static string GetQueryKey(string operation, IList parameters, OperationReturnType returnType)
{
- var combined = new StringBuilder();
- combined.Append(returnType.ToString()).Append("/").Append(operation).Append("/").Append(parameters.OrderBy(p => p.Name).Select(p => $"{p.Name}={p.Value}").ToDelimitedString(","));
- var hash = Hash.Compute(Encoding.UTF8.GetBytes(combined.ToString()));
- return typeof(T).FullName + "/" + hash;
+ var sb = new StringBuilder();
+ sb.Append(returnType.ToString()).Append("/").Append(operation);
+ if (parameters.Count > 0)
+ {
+ sb.Append("/");
+ var sorted = parameters.Count > 1;
+ IEnumerable paramSource = sorted ? parameters.OrderBy(p => p.Name) : parameters;
+ var first = true;
+ foreach (var p in paramSource)
+ {
+ if (!first) sb.Append(",");
+ sb.Append(p.Name).Append("=").Append(p.Value);
+ first = false;
+ }
+ }
+ var hash = Hash.Compute(Encoding.UTF8.GetBytes(sb.ToString()));
+ return string.Concat(typeof(T).FullName, "/", hash.ToString());
}
///
@@ -344,9 +351,7 @@ private static async Task> RetrieveImplemenationAsync();
- queryKey = GetQueryKey(operation, parameters ?? new Param[] { }, returnType);
-
- Log.CaptureBegin($"Retrieving from L1 cache: {queryKey}", config);
+ queryKey = GetQueryKey(operation, parameters ?? Array.Empty(), returnType);
if (returnType == OperationReturnType.MultiResult)
{
@@ -360,8 +365,6 @@ private static async Task> RetrieveImplemenationAsync> RetrieveImplemenationAsync ConvertDataReader(IDataReader reader, Func ConvertDataReader(IDataReader reader, Func(isInterface);
- var record = (IDataRecord)new WrappedRecord(reader, columns);
Map(record, item, config.AutoTypeCoercion);
TrySetObjectState(item);
@@ -1217,6 +1224,7 @@ void changeSkipNext()
var isSimpleType = reflectedTypes[resultIndex].IsSimpleType;
var useMapper = !isInterface || config.DefaultMaterializationMode == MaterializationMode.Exact;
var columns = !isSimpleType ? reader.GetColumns() : null;
+ var wrappedReader = useMapper && !isSimpleType && !isAnonymous ? (object)new WrappedReader(reader, columns) : null;
while (reader.Read())
{
if (isSimpleType)
@@ -1231,7 +1239,7 @@ void changeSkipNext()
}
else if (useMapper)
{
- var item = Map((object)new WrappedReader(reader, columns), types[resultIndex], config.AutoTypeCoercion);
+ var item = Map(wrappedReader, types[resultIndex], config.AutoTypeCoercion);
TrySetObjectState(item);
yield return new MultiResultItem { Item = item, ItemType = types[resultIndex], ItemTypeIndex = resultIndex, SkipNextCallback = changeSkipNext };
}
diff --git a/src/Nemo/Utilities/Hash.cs b/src/Nemo/Utilities/Hash.cs
index 0fd37f5..b489feb 100644
--- a/src/Nemo/Utilities/Hash.cs
+++ b/src/Nemo/Utilities/Hash.cs
@@ -2,70 +2,65 @@
{
public class Hash
{
- private static uint _a;
- private static uint _b;
- private static uint _c;
-
- private static void Mix()
+ private static void Mix(ref uint a, ref uint b, ref uint c)
{
- _a -= _b; _a -= _c; _a ^= (_c >> 13);
- _b -= _c; _b -= _a; _b ^= (_a << 8);
- _c -= _a; _c -= _b; _c ^= (_b >> 13);
- _a -= _b; _a -= _c; _a ^= (_c >> 12);
- _b -= _c; _b -= _a; _b ^= (_a << 16);
- _c -= _a; _c -= _b; _c ^= (_b >> 5);
- _a -= _b; _a -= _c; _a ^= (_c >> 3);
- _b -= _c; _b -= _a; _b ^= (_a << 10);
- _c -= _a; _c -= _b; _c ^= (_b >> 15);
+ a -= b; a -= c; a ^= (c >> 13);
+ b -= c; b -= a; b ^= (a << 8);
+ c -= a; c -= b; c ^= (b >> 13);
+ a -= b; a -= c; a ^= (c >> 12);
+ b -= c; b -= a; b ^= (a << 16);
+ c -= a; c -= b; c ^= (b >> 5);
+ a -= b; a -= c; a ^= (c >> 3);
+ b -= c; b -= a; b ^= (a << 10);
+ c -= a; c -= b; c ^= (b >> 15);
}
public static uint Compute(byte[] data)
{
var len = data.Length;
- _a = _b = 0x9e3779b9;
- _c = 0;
+ uint a = 0x9e3779b9, b = 0x9e3779b9, c = 0;
var i = 0;
while (i + 12 <= len)
{
- _a += data[i++] |
+ a += data[i++] |
((uint)data[i++] << 8) |
((uint)data[i++] << 16) |
((uint)data[i++] << 24);
- _b += data[i++] |
+ b += data[i++] |
((uint)data[i++] << 8) |
((uint)data[i++] << 16) |
((uint)data[i++] << 24);
- _c += data[i++] |
+ c += data[i++] |
((uint)data[i++] << 8) |
((uint)data[i++] << 16) |
((uint)data[i++] << 24);
- Mix();
+ Mix(ref a, ref b, ref c);
}
- _c += (uint)len;
+ c += (uint)len;
if (i < len)
- _a += data[i++];
+ a += data[i++];
if (i < len)
- _a += (uint)data[i++] << 8;
+ a += (uint)data[i++] << 8;
if (i < len)
- _a += (uint)data[i++] << 16;
+ a += (uint)data[i++] << 16;
if (i < len)
- _a += (uint)data[i++] << 24;
+ a += (uint)data[i++] << 24;
if (i < len)
- _b += data[i++];
+ b += data[i++];
if (i < len)
- _b += (uint)data[i++] << 8;
+ b += (uint)data[i++] << 8;
if (i < len)
- _b += (uint)data[i++] << 16;
+ b += (uint)data[i++] << 16;
if (i < len)
- _b += (uint)data[i++] << 24;
+ b += (uint)data[i++] << 24;
if (i < len)
- _c += (uint)data[i++] << 8;
+ c += (uint)data[i++] << 8;
if (i < len)
- _c += (uint)data[i++] << 16;
+ c += (uint)data[i++] << 16;
if (i < len)
- _c += (uint)data[i++] << 24;
- Mix();
- return _c;
+ c += (uint)data[i++] << 24;
+ Mix(ref a, ref b, ref c);
+ return c;
}
}
}
From b042d1a0283204dd791165e7d567ae64694b3996 Mon Sep 17 00:00:00 2001
From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Thu, 12 Feb 2026 17:46:10 +0000
Subject: [PATCH 2/3] Revert WrappedRecord/WrappedReader hoisting: reader is an
iterator and cannot be shared across rows
Co-Authored-By: Max Stepanskiy
---
src/Nemo/ObjectFactory.cs | 5 ++---
1 file changed, 2 insertions(+), 3 deletions(-)
diff --git a/src/Nemo/ObjectFactory.cs b/src/Nemo/ObjectFactory.cs
index be51212..37fccb0 100644
--- a/src/Nemo/ObjectFactory.cs
+++ b/src/Nemo/ObjectFactory.cs
@@ -1077,7 +1077,6 @@ private static IEnumerable ConvertDataReader(IDataReader reader, Func ConvertDataReader(IDataReader reader, Func(isInterface);
+ var record = (IDataRecord)new WrappedRecord(reader, columns);
Map(record, item, config.AutoTypeCoercion);
TrySetObjectState(item);
@@ -1224,7 +1224,6 @@ void changeSkipNext()
var isSimpleType = reflectedTypes[resultIndex].IsSimpleType;
var useMapper = !isInterface || config.DefaultMaterializationMode == MaterializationMode.Exact;
var columns = !isSimpleType ? reader.GetColumns() : null;
- var wrappedReader = useMapper && !isSimpleType && !isAnonymous ? (object)new WrappedReader(reader, columns) : null;
while (reader.Read())
{
if (isSimpleType)
@@ -1239,7 +1238,7 @@ void changeSkipNext()
}
else if (useMapper)
{
- var item = Map(wrappedReader, types[resultIndex], config.AutoTypeCoercion);
+ var item = Map((object)new WrappedReader(reader, columns), types[resultIndex], config.AutoTypeCoercion);
TrySetObjectState(item);
yield return new MultiResultItem { Item = item, ItemType = types[resultIndex], ItemTypeIndex = resultIndex, SkipNextCallback = changeSkipNext };
}
From b0829dbf0f8eccffe517a4358f4e4b06746e8c3f Mon Sep 17 00:00:00 2001
From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Thu, 12 Feb 2026 18:06:08 +0000
Subject: [PATCH 3/3] Deep performance optimizations for Select and Retrieve
hot paths
- 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
---
src/Nemo/DataReaderExtensions.cs | 6 ++-
src/Nemo/ObjectFactory.cs | 90 ++++++++++++++++++++++++--------
2 files changed, 72 insertions(+), 24 deletions(-)
diff --git a/src/Nemo/DataReaderExtensions.cs b/src/Nemo/DataReaderExtensions.cs
index 4556536..2c1ea73 100644
--- a/src/Nemo/DataReaderExtensions.cs
+++ b/src/Nemo/DataReaderExtensions.cs
@@ -8,8 +8,12 @@ internal static class DataReaderExtensions
{
internal static ISet GetColumns(this IDataRecord record)
{
- var columns = new HashSet(StringComparer.OrdinalIgnoreCase);
int count = record.FieldCount;
+#if NETSTANDARD2_0 || NET472
+ var columns = new HashSet(StringComparer.OrdinalIgnoreCase);
+#else
+ var columns = new HashSet(count, StringComparer.OrdinalIgnoreCase);
+#endif
for (var i = 0; i < count; i++)
{
columns.Add(record.GetName(i));
diff --git a/src/Nemo/ObjectFactory.cs b/src/Nemo/ObjectFactory.cs
index 37fccb0..de41e3a 100644
--- a/src/Nemo/ObjectFactory.cs
+++ b/src/Nemo/ObjectFactory.cs
@@ -1077,6 +1077,19 @@ private static IEnumerable ConvertDataReader(IDataReader reader, Func 1 && useMapper)
+ {
+ secondaryMappers = new Mapper.PropertyMapper[typeCount];
+ for (var i = 1; i < typeCount; i++)
+ {
+ secondaryMappers[i] = Mapper.CreateDelegate(typeof(IDataRecord), types[i], true, autoTypeCoercion);
+ }
+ }
while (reader.Read())
{
if (isSimpleType)
@@ -1092,20 +1105,23 @@ private static IEnumerable ConvertDataReader(IDataReader reader, Func(isInterface);
var record = (IDataRecord)new WrappedRecord(reader, columns);
- Map(record, item, config.AutoTypeCoercion);
+ if (autoTypeCoercion)
+ FastIndexerMapperWithTypeCoercion.Map(record, item);
+ else
+ FastIndexerMapper.Map(record, item);
TrySetObjectState(item);
- if (map != null)
+ if (hasMap)
{
- var args = new object[types.Count];
args[0] = item;
- for (var i = 1; i < types.Count; i++)
+ for (var i = 1; i < typeCount; i++)
{
var identity = CreateIdentity(types[i], reader);
if (!references.TryGetValue(identity, out var reference))
{
- reference = Map((object)record, types[i], config.AutoTypeCoercion);
+ reference = Create(types[i]);
+ secondaryMappers[i](record, reference);
TrySetObjectState(reference);
references.Add(identity, reference);
}
@@ -1133,11 +1149,10 @@ private static IEnumerable ConvertDataReader(IDataReader reader, Func ConvertDataReader(IDataReader reader, Func CreateDynamicItem(IDataReader reader,
return bag;
}
+ private static readonly ConcurrentDictionary PrimaryKeyColumnCache = new ConcurrentDictionary();
+
+ private static string[] GetPrimaryKeyColumnNames(Type objectType)
+ {
+ return PrimaryKeyColumnCache.GetOrAdd(objectType, type =>
+ {
+ var nameMap = Reflector.GetPropertyNameMap(type);
+ return nameMap.Values.Where(p => p.IsPrimaryKey)
+ .Select(p => p.MappedColumnName ?? p.PropertyName)
+ .OrderBy(_ => _)
+ .ToArray();
+ });
+ }
+
private static Tuple CreateIdentity(Type objectType, IDataRecord record)
{
- var nameMap = Reflector.GetPropertyNameMap(objectType);
- var identity = Tuple.Create(objectType, string.Join(",", nameMap.Values.Where(p => p.IsPrimaryKey)
- .Select(p => p.MappedColumnName ?? p.PropertyName)
- .OrderBy(_ => _)
- .Select(n => Convert.ToString(record.GetValue(record.GetOrdinal(n))))));
- return identity;
+ var pkColumns = GetPrimaryKeyColumnNames(objectType);
+ var sb = new StringBuilder();
+ for (var i = 0; i < pkColumns.Length; i++)
+ {
+ if (i > 0) sb.Append(',');
+ sb.Append(Convert.ToString(record.GetValue(record.GetOrdinal(pkColumns[i]))));
+ }
+ return Tuple.Create(objectType, sb.ToString());
}
private static IEnumerable ConvertDataReaderMultiResult(IDataReader reader, IList types, INemoConfiguration config)
@@ -1212,7 +1243,13 @@ void changeSkipNext()
skipNext = true;
}
- var reflectedTypes = types.Select(t => Reflector.GetReflectedType(t)).ToList();
+ var reflectedTypes = new ReflectedType[types.Count];
+ for (var i = 0; i < types.Count; i++)
+ {
+ reflectedTypes[i] = Reflector.GetReflectedType(types[i]);
+ }
+
+ var autoTypeCoercion = config.AutoTypeCoercion;
try
{
@@ -1224,30 +1261,37 @@ void changeSkipNext()
var isSimpleType = reflectedTypes[resultIndex].IsSimpleType;
var useMapper = !isInterface || config.DefaultMaterializationMode == MaterializationMode.Exact;
var columns = !isSimpleType ? reader.GetColumns() : null;
+ var currentType = types[resultIndex];
+ Mapper.PropertyMapper resultMapper = null;
+ if (useMapper && !isSimpleType && !isAnonymous)
+ {
+ resultMapper = Mapper.CreateDelegate(typeof(IDataRecord), currentType, true, autoTypeCoercion);
+ }
while (reader.Read())
{
if (isSimpleType)
{
var item = reader.GetValue(0);
- yield return new MultiResultItem { Item = Reflector.ChangeType(item, types[resultIndex]), ItemType = types[resultIndex], ItemTypeIndex = resultIndex, SkipNextCallback = changeSkipNext };
+ yield return new MultiResultItem { Item = Reflector.ChangeType(item, currentType), ItemType = currentType, ItemTypeIndex = resultIndex, SkipNextCallback = changeSkipNext };
}
else if (isAnonymous)
{
var item = CreateDynamicItem(reader, columns, true);
- yield return new MultiResultItem { Item = item, ItemType = types[resultIndex], ItemTypeIndex = resultIndex, SkipNextCallback = changeSkipNext };
+ yield return new MultiResultItem { Item = item, ItemType = currentType, ItemTypeIndex = resultIndex, SkipNextCallback = changeSkipNext };
}
else if (useMapper)
{
- var item = Map((object)new WrappedReader(reader, columns), types[resultIndex], config.AutoTypeCoercion);
- TrySetObjectState(item);
- yield return new MultiResultItem { Item = item, ItemType = types[resultIndex], ItemTypeIndex = resultIndex, SkipNextCallback = changeSkipNext };
+ var target = Create(currentType);
+ resultMapper(new WrappedReader(reader, columns), target);
+ TrySetObjectState(target);
+ yield return new MultiResultItem { Item = target, ItemType = currentType, ItemTypeIndex = resultIndex, SkipNextCallback = changeSkipNext };
}
else
{
var bag = CreateDynamicItem(reader, columns, false);
- var item = Wrap(bag, types[resultIndex]);
+ var item = Wrap(bag, currentType);
TrySetObjectState(item);
- yield return new MultiResultItem { Item = item, ItemType = types[resultIndex], ItemTypeIndex = resultIndex, SkipNextCallback = changeSkipNext };
+ yield return new MultiResultItem { Item = item, ItemType = currentType, ItemTypeIndex = resultIndex, SkipNextCallback = changeSkipNext };
}
if (skipNext)