Skip to content

Commit ab8f887

Browse files
committed
Should improve hash key stability
1 parent 8f4b317 commit ab8f887

File tree

3 files changed

+172
-28
lines changed

3 files changed

+172
-28
lines changed

src/Constructors/CachedConstructors.cs

Lines changed: 54 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ namespace Soenneker.Reflection.Cache.Constructors;
1717
public sealed class CachedConstructors : ICachedConstructors
1818
{
1919
private readonly Lazy<CachedConstructor[]> _cachedArray;
20-
private readonly Lazy<FrozenDictionary<int, CachedConstructor>> _cachedDict;
20+
private readonly Lazy<FrozenDictionary<string, CachedConstructor[]>> _byName;
2121
private readonly Lazy<ConstructorInfo?[]> _cachedConstructorInfos;
2222

2323
// Fast path for parameterless construction (if available)
@@ -44,16 +44,27 @@ public CachedConstructors(CachedType cachedType, CachedTypes cachedTypes, bool t
4444
return result;
4545
}, mode);
4646

47-
_cachedDict = new Lazy<FrozenDictionary<int, CachedConstructor>>(() =>
47+
_byName = new Lazy<FrozenDictionary<string, CachedConstructor[]>>(() =>
4848
{
4949
CachedConstructor[] arr = _cachedArray.Value;
50-
var dict = new Dictionary<int, CachedConstructor>(arr.Length);
50+
var map = new Dictionary<string, List<CachedConstructor>>(StringComparer.Ordinal);
5151
for (var i = 0; i < arr.Length; i++)
5252
{
53-
dict[arr[i].ToHashKey()] = arr[i]; // last-one-wins for duplicate sig hashes
53+
ConstructorInfo? ci = arr[i].ConstructorInfo;
54+
string name = ci is null ? string.Empty : ci.Name; // .ctor/.cctor
55+
if (!map.TryGetValue(name, out List<CachedConstructor>? list))
56+
{
57+
list = new List<CachedConstructor>();
58+
map[name] = list;
59+
}
60+
list.Add(arr[i]);
5461
}
5562

56-
return dict.ToFrozenDictionary();
63+
var frozen = new Dictionary<string, CachedConstructor[]>(map.Count, StringComparer.Ordinal);
64+
foreach (KeyValuePair<string, List<CachedConstructor>> kvp in map)
65+
frozen[kvp.Key] = kvp.Value.ToArray();
66+
67+
return frozen.ToFrozenDictionary(StringComparer.Ordinal);
5768
}, mode);
5869

5970
_cachedConstructorInfos = new Lazy<ConstructorInfo?[]>(() => _cachedArray.Value.ToConstructorInfos(), mode);
@@ -82,8 +93,44 @@ public CachedConstructors(CachedType cachedType, CachedTypes cachedTypes, bool t
8293
[MethodImpl(MethodImplOptions.AggressiveInlining)]
8394
public CachedConstructor? GetCachedConstructor(Type[]? parameterTypes = null)
8495
{
85-
int key = parameterTypes.ToHashKey();
86-
return _cachedDict.Value.GetValueOrDefault(key);
96+
if (parameterTypes == null || parameterTypes.Length == 0)
97+
{
98+
// Prefer parameterless .ctor if present
99+
if (_byName.Value.TryGetValue(".ctor", out CachedConstructor[]? ctors))
100+
{
101+
for (var i = 0; i < ctors.Length; i++)
102+
{
103+
if (ctors[i].GetParameters().Length == 0)
104+
return ctors[i];
105+
}
106+
}
107+
return null;
108+
}
109+
110+
if (!_byName.Value.TryGetValue(".ctor", out CachedConstructor[]? candidates) || candidates.Length == 0)
111+
return null;
112+
113+
for (var i = 0; i < candidates.Length; i++)
114+
{
115+
var ps = candidates[i].GetParameters();
116+
if (ps.Length != parameterTypes.Length)
117+
continue;
118+
119+
var match = true;
120+
for (var j = 0; j < ps.Length; j++)
121+
{
122+
if (!ReferenceEquals(ps[j].ParameterType, parameterTypes[j]))
123+
{
124+
match = false;
125+
break;
126+
}
127+
}
128+
129+
if (match)
130+
return candidates[i];
131+
}
132+
133+
return null;
87134
}
88135

89136
[MethodImpl(MethodImplOptions.AggressiveInlining)]

src/Methods/CachedMethod.cs

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ public CachedMethod(MethodInfo? methodInfo, CachedTypes cachedTypes, bool thread
5555
);
5656

5757
_invoker = new Lazy<Func<object?, object?[]?, object?>>(
58-
() => BuildInvoker(methodInfo),
58+
() => BuildSafeInvoker(methodInfo),
5959
threadSafe
6060
);
6161
}
@@ -144,13 +144,29 @@ public object[] GetCustomAttributes()
144144

145145
// -------- internals --------
146146

147-
private static Func<object?, object?[]?, object?> BuildInvoker(MethodInfo mi)
147+
private static Func<object?, object?[]?, object?> BuildSafeInvoker(MethodInfo mi)
148148
{
149+
// Fallback to MethodInfo.Invoke for byref/byref-like signatures or on any compile failure
150+
ParameterInfo[] parmsProbe = mi.GetParameters();
151+
for (var i = 0; i < parmsProbe.Length; i++)
152+
{
153+
Type pt = parmsProbe[i].ParameterType;
154+
if (pt.IsByRef)
155+
{
156+
return (instance, args) => mi.Invoke(instance, args ?? Array.Empty<object?>());
157+
}
158+
// .NET doesn't expose IsByRefLike directly pre .NET 7 on Type, but common cases are Span/ReadOnlySpan
159+
if (pt.FullName is not null && (pt.FullName.StartsWith("System.Span`1", StringComparison.Ordinal) || pt.FullName.StartsWith("System.ReadOnlySpan`1", StringComparison.Ordinal)))
160+
{
161+
return (instance, args) => mi.Invoke(instance, args ?? Array.Empty<object?>());
162+
}
163+
}
164+
149165
// Build: (object? instance, object?[]? args) => (object?) <call>
150166
ParameterExpression instParam = Expression.Parameter(typeof(object), "instance");
151167
ParameterExpression argsParam = Expression.Parameter(typeof(object[]), "args");
152168

153-
ParameterInfo[] parms = mi.GetParameters();
169+
ParameterInfo[] parms = parmsProbe;
154170
var callArgs = new Expression[parms.Length];
155171

156172
for (var i = 0; i < parms.Length; i++)
@@ -176,8 +192,16 @@ public object[] GetCustomAttributes()
176192
body = Expression.Block(body); // nothing extra; CreateDelegate handles fine with null args
177193
}
178194

179-
Expression<Func<object?, object?[]?, object?>> lambda = Expression.Lambda<Func<object?, object?[]?, object?>>(body, instParam, argsParam);
180-
return lambda.Compile(); // Tiered JIT will optimize quickly under load
195+
try
196+
{
197+
Expression<Func<object?, object?[]?, object?>> lambda = Expression.Lambda<Func<object?, object?[]?, object?>>(body, instParam, argsParam);
198+
return lambda.Compile(); // Tiered JIT will optimize quickly under load
199+
}
200+
catch
201+
{
202+
// Safe fallback
203+
return (instance, args) => mi.Invoke(instance, args ?? Array.Empty<object?>());
204+
}
181205
}
182206

183207
/// Abstraction so we can use the fastest structure depending on thread-safety.

src/Methods/CachedMethods.cs

Lines changed: 89 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -16,21 +16,21 @@ public sealed class CachedMethods : ICachedMethods
1616
private readonly CachedType _cachedType;
1717
private readonly CachedTypes _cachedTypes;
1818

19-
// Build all three artifacts in one go, once.
19+
// Build all artifacts in one go, once.
2020
private readonly Lazy<BuiltCache> _built;
2121

2222
private readonly bool _threadSafe;
2323

2424
private sealed class BuiltCache
2525
{
2626
public readonly CachedMethod[] Methods;
27-
public readonly FrozenDictionary<int, CachedMethod> Index;
27+
public readonly FrozenDictionary<string, CachedMethod[]> MethodsByName;
2828
public readonly MethodInfo?[] MethodInfos;
2929

30-
public BuiltCache(CachedMethod[] methods, FrozenDictionary<int, CachedMethod> index, MethodInfo?[] infos)
30+
public BuiltCache(CachedMethod[] methods, FrozenDictionary<string, CachedMethod[]> methodsByName, MethodInfo?[] infos)
3131
{
3232
Methods = methods;
33-
Index = index;
33+
MethodsByName = methodsByName;
3434
MethodInfos = infos;
3535
}
3636
}
@@ -51,27 +51,39 @@ private BuiltCache BuildAll()
5151
int count = methodInfos.Length;
5252

5353
var methods = new CachedMethod[count];
54-
var dict = new Dictionary<int, CachedMethod>(count);
54+
var byName = new Dictionary<string, List<CachedMethod>>(StringComparer.Ordinal);
5555

5656
for (var i = 0; i < count; i++)
5757
{
5858
var cm = new CachedMethod(methodInfos[i], _cachedTypes, _threadSafe);
5959
methods[i] = cm;
60-
dict.Add(cm.ToHashKey(), cm);
60+
61+
string name = cm.Name!;
62+
if (!byName.TryGetValue(name, out List<CachedMethod>? list))
63+
{
64+
list = new List<CachedMethod>();
65+
byName[name] = list;
66+
}
67+
list.Add(cm);
6168
}
6269

63-
// Freeze for faster, allocation-friendly lookups
64-
FrozenDictionary<int, CachedMethod> frozen = dict.ToFrozenDictionary();
70+
// Freeze name map
71+
var frozenByName = new Dictionary<string, CachedMethod[]>(byName.Count, StringComparer.Ordinal);
72+
foreach (KeyValuePair<string, List<CachedMethod>> kvp in byName)
73+
{
74+
frozenByName[kvp.Key] = kvp.Value.ToArray();
75+
}
76+
77+
FrozenDictionary<string, CachedMethod[]> methodsByName = frozenByName.ToFrozenDictionary(StringComparer.Ordinal);
6578

6679
// MethodInfos array without extra enumerations
6780
var infos = new MethodInfo?[count];
68-
6981
for (var i = 0; i < count; i++)
7082
{
7183
infos[i] = methods[i].MethodInfo;
7284
}
7385

74-
return new BuiltCache(methods, frozen, infos);
86+
return new BuiltCache(methods, methodsByName, infos);
7587
}
7688

7789
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -89,21 +101,82 @@ private BuiltCache BuildAll()
89101
[MethodImpl(MethodImplOptions.AggressiveInlining)]
90102
public CachedMethod? GetCachedMethod(string name)
91103
{
92-
int key = ReflectionCacheUtil.GetCacheKeyForMethod(name);
93-
return _built.Value.Index.GetValueOrDefault(key);
104+
if (!_built.Value.MethodsByName.TryGetValue(name, out CachedMethod[]? candidates) || candidates.Length == 0)
105+
return null;
106+
107+
// If there is a single candidate, return it; otherwise prefer parameterless
108+
if (candidates.Length == 1)
109+
return candidates[0];
110+
111+
for (var i = 0; i < candidates.Length; i++)
112+
{
113+
ParameterInfo[]? ps = candidates[i].GetParameters();
114+
if (ps.Length == 0)
115+
return candidates[i];
116+
}
117+
118+
// Fallback: first candidate
119+
return candidates[0];
94120
}
95121

96122
[MethodImpl(MethodImplOptions.AggressiveInlining)]
97123
public CachedMethod? GetCachedMethod(string name, Type[] parameterTypes)
98124
{
99-
int key = ReflectionCacheUtil.GetCacheKeyForMethod(name, parameterTypes);
100-
return _built.Value.Index.GetValueOrDefault(key);
125+
if (!_built.Value.MethodsByName.TryGetValue(name, out CachedMethod[]? candidates) || candidates.Length == 0)
126+
return null;
127+
128+
for (var i = 0; i < candidates.Length; i++)
129+
{
130+
ParameterInfo[] ps = candidates[i].GetParameters();
131+
if (ps.Length != parameterTypes.Length)
132+
continue;
133+
134+
var match = true;
135+
for (var j = 0; j < ps.Length; j++)
136+
{
137+
if (!ReferenceEquals(ps[j].ParameterType, parameterTypes[j]))
138+
{
139+
match = false;
140+
break;
141+
}
142+
}
143+
144+
if (match)
145+
return candidates[i];
146+
}
147+
148+
return null;
101149
}
102150

103151
[MethodImpl(MethodImplOptions.AggressiveInlining)]
104152
public CachedMethod? GetCachedMethod(string name, CachedType[] cachedParameterTypes)
105153
{
106-
int key = ReflectionCacheUtil.GetCacheKeyForMethodWithCachedParameterTypes(name, cachedParameterTypes);
107-
return _built.Value.Index.GetValueOrDefault(key);
154+
if (!_built.Value.MethodsByName.TryGetValue(name, out CachedMethod[]? candidates) || candidates.Length == 0)
155+
return null;
156+
157+
int len = cachedParameterTypes?.Length ?? 0;
158+
159+
for (var i = 0; i < candidates.Length; i++)
160+
{
161+
ParameterInfo[] ps = candidates[i].GetParameters();
162+
if (ps.Length != len)
163+
continue;
164+
165+
var match = true;
166+
for (var j = 0; j < len; j++)
167+
{
168+
Type? t = cachedParameterTypes[j].Type;
169+
if (!ReferenceEquals(ps[j].ParameterType, t))
170+
{
171+
match = false;
172+
break;
173+
}
174+
}
175+
176+
if (match)
177+
return candidates[i];
178+
}
179+
180+
return null;
108181
}
109182
}

0 commit comments

Comments
 (0)