Skip to content

Commit 01c6bb4

Browse files
committed
performance and allocation improvements
1 parent 354f49a commit 01c6bb4

File tree

19 files changed

+708
-658
lines changed

19 files changed

+708
-658
lines changed
Lines changed: 64 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -1,157 +1,120 @@
1-
using System;
2-
using System.Collections.Generic;
3-
using System.Reflection;
4-
using Soenneker.Extensions.Array.Object;
1+
using Soenneker.Extensions.Array.Object;
52
using Soenneker.Extensions.Type.Array;
63
using Soenneker.Reflection.Cache.Constructors.Abstract;
74
using Soenneker.Reflection.Cache.Extensions;
85
using Soenneker.Reflection.Cache.Types;
6+
using System;
7+
using System.Collections.Frozen;
8+
using System.Collections.Generic;
9+
using System.Linq.Expressions;
10+
using System.Reflection;
11+
using System.Runtime.CompilerServices;
12+
using System.Threading;
913

1014
namespace Soenneker.Reflection.Cache.Constructors;
1115

12-
///<inheritdoc cref="ICachedConstructors"/>
16+
/// <inheritdoc cref="ICachedConstructors"/>
1317
public sealed class CachedConstructors : ICachedConstructors
1418
{
1519
private readonly Lazy<CachedConstructor[]> _cachedArray;
16-
private readonly Lazy<Dictionary<int, CachedConstructor>> _cachedDict;
17-
18-
private readonly CachedType _cachedType;
19-
private readonly CachedTypes _cachedTypes;
20-
20+
private readonly Lazy<FrozenDictionary<int, CachedConstructor>> _cachedDict;
2121
private readonly Lazy<ConstructorInfo?[]> _cachedConstructorInfos;
2222

23+
// Fast path for parameterless construction (if available)
24+
private readonly Lazy<Func<object?>> _parameterlessActivator;
25+
2326
public CachedConstructors(CachedType cachedType, CachedTypes cachedTypes, bool threadSafe = true)
2427
{
25-
_cachedType = cachedType;
26-
_cachedTypes = cachedTypes;
28+
CachedType cachedType1 = cachedType;
29+
CachedTypes cachedTypes1 = cachedTypes;
2730

28-
_cachedArray = new Lazy<CachedConstructor[]>(() => SetArray(threadSafe), threadSafe);
29-
_cachedDict = new Lazy<Dictionary<int, CachedConstructor>>(() => SetDict(threadSafe), threadSafe);
30-
_cachedDict = new Lazy<Dictionary<int, CachedConstructor>>(() => SetDict(threadSafe), threadSafe);
31+
LazyThreadSafetyMode mode = threadSafe ? LazyThreadSafetyMode.ExecutionAndPublication : LazyThreadSafetyMode.None;
3132

32-
_cachedConstructorInfos = new Lazy<ConstructorInfo?[]>(() => _cachedArray.Value.ToConstructorInfos(), threadSafe);
33-
}
33+
var constructorInfos = new Lazy<ConstructorInfo[]>(() => cachedType1.Type!.GetConstructors(cachedTypes1.Options.ConstructorFlags), mode);
3434

35-
public CachedConstructor? GetCachedConstructor(Type[]? parameterTypes = null)
36-
{
37-
int key = parameterTypes.ToHashKey();
38-
return _cachedDict.Value.GetValueOrDefault(key);
39-
}
40-
41-
public ConstructorInfo? GetConstructor(Type[]? parameterTypes = null)
42-
{
43-
return GetCachedConstructor(parameterTypes)?.ConstructorInfo;
44-
}
45-
46-
private CachedConstructor[] SetArray(bool threadSafe)
47-
{
48-
if (_cachedDict.IsValueCreated)
35+
_cachedArray = new Lazy<CachedConstructor[]>(() =>
4936
{
50-
Dictionary<int, CachedConstructor>.ValueCollection cachedDictValues = _cachedDict.Value.Values;
51-
var result = new CachedConstructor[cachedDictValues.Count];
52-
var i = 0;
53-
54-
foreach (CachedConstructor constructor in cachedDictValues)
37+
ConstructorInfo[] infos = constructorInfos.Value;
38+
var result = new CachedConstructor[infos.Length];
39+
for (var i = 0; i < infos.Length; i++)
5540
{
56-
result[i++] = constructor;
41+
result[i] = new CachedConstructor(infos[i], cachedTypes1, threadSafe);
5742
}
5843

5944
return result;
60-
}
61-
62-
ConstructorInfo[] constructorInfos = _cachedType.Type!.GetConstructors(_cachedTypes.Options.ConstructorFlags);
63-
int length = constructorInfos.Length;
64-
65-
var cachedConstructors = new CachedConstructor[length];
66-
67-
for (var i = 0; i < length; i++)
68-
{
69-
cachedConstructors[i] = new CachedConstructor(constructorInfos[i], _cachedTypes, threadSafe);
70-
}
71-
72-
return cachedConstructors;
73-
}
45+
}, mode);
7446

75-
private Dictionary<int, CachedConstructor> SetDict(bool threadSafe)
76-
{
77-
if (_cachedArray.IsValueCreated)
47+
_cachedDict = new Lazy<FrozenDictionary<int, CachedConstructor>>(() =>
7848
{
79-
CachedConstructor[] cachedArrayValue = _cachedArray.Value;
80-
int length = cachedArrayValue.Length;
81-
82-
var dict = new Dictionary<int, CachedConstructor>(length);
83-
84-
for (var i = 0; i < length; i++)
49+
CachedConstructor[] arr = _cachedArray.Value;
50+
var dict = new Dictionary<int, CachedConstructor>(arr.Length);
51+
for (var i = 0; i < arr.Length; i++)
8552
{
86-
int key = cachedArrayValue[i].ToHashKey();
87-
dict[key] = cachedArrayValue[i];
53+
dict[arr[i].ToHashKey()] = arr[i]; // last-one-wins for duplicate sig hashes
8854
}
8955

90-
return dict;
91-
}
92-
93-
ConstructorInfo[] constructorInfos = _cachedType.Type!.GetConstructors(_cachedTypes.Options.ConstructorFlags);
94-
int constructorInfosLength = constructorInfos.Length;
56+
return dict.ToFrozenDictionary();
57+
}, mode);
9558

96-
var constructorsDict = new Dictionary<int, CachedConstructor>(constructorInfosLength);
59+
_cachedConstructorInfos = new Lazy<ConstructorInfo?[]>(() => _cachedArray.Value.ToConstructorInfos(), mode);
9760

98-
ReadOnlySpan<ConstructorInfo> constructorsSpan = constructorInfos;
99-
100-
for (var i = 0; i < constructorInfosLength; i++)
61+
_parameterlessActivator = new Lazy<Func<object?>>(() =>
10162
{
102-
ConstructorInfo info = constructorsSpan[i];
103-
104-
ParameterInfo[] parameters = info.GetParameters();
105-
106-
int parametersLength = parameters.Length;
63+
Type type = cachedType1.Type!;
64+
ConstructorInfo? ctor = type.GetConstructor(Type.EmptyTypes);
65+
if (ctor is null)
66+
return static () => null;
10767

108-
var parameterTypes = new Type[parametersLength];
109-
110-
ReadOnlySpan<ParameterInfo> parametersSpan = parameters;
111-
112-
for (var j = 0; j < parametersLength; j++)
68+
try
11369
{
114-
parameterTypes[j] = parametersSpan[j].ParameterType;
70+
// Build: () => new T()
71+
NewExpression newExpr = Expression.New(ctor);
72+
Expression<Func<object?>> lambda = Expression.Lambda<Func<object?>>(Expression.Convert(newExpr, typeof(object)));
73+
return lambda.Compile();
11574
}
116-
117-
int key = parameterTypes.ToHashKey();
118-
119-
constructorsDict[key] = new CachedConstructor(info, _cachedTypes, threadSafe);
120-
}
121-
122-
return constructorsDict;
75+
catch
76+
{
77+
return () => Activator.CreateInstance(type);
78+
}
79+
}, mode);
12380
}
12481

125-
public CachedConstructor[] GetCachedConstructors()
82+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
83+
public CachedConstructor? GetCachedConstructor(Type[]? parameterTypes = null)
12684
{
127-
return _cachedArray.Value;
85+
int key = parameterTypes.ToHashKey();
86+
return _cachedDict.Value.GetValueOrDefault(key);
12887
}
12988

130-
public ConstructorInfo?[] GetConstructors()
131-
{
132-
return _cachedConstructorInfos.Value;
133-
}
89+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
90+
public ConstructorInfo? GetConstructor(Type[]? parameterTypes = null) => GetCachedConstructor(parameterTypes)?.ConstructorInfo;
91+
92+
public CachedConstructor[] GetCachedConstructors() => _cachedArray.Value;
93+
94+
public ConstructorInfo?[] GetConstructors() => _cachedConstructorInfos.Value;
13495

13596
public object? CreateInstance()
13697
{
137-
//TODO: One day parameterless invoke of the constructorInfo may be faster than Activator.CreateInstance
138-
return Activator.CreateInstance(_cachedType.Type!);
98+
// Use the cached parameterless activator if present; otherwise null if no default ctor.
99+
Func<object?> f = _parameterlessActivator.Value;
100+
return f();
139101
}
140102

141103
public T? CreateInstance<T>()
142104
{
143-
return (T?)Activator.CreateInstance(_cachedType.Type!);
105+
object? obj = CreateInstance();
106+
return obj is null ? default : (T?) obj;
144107
}
145-
108+
146109
public object? CreateInstance(params object[] parameters)
147110
{
148111
if (parameters.Length == 0)
149112
return CreateInstance();
150113

114+
// Avoid a LINQ pass; reuse your extension which is likely optimized.
151115
Type[] parameterTypes = parameters.ToTypes();
152116

153117
CachedConstructor? cachedConstructor = GetCachedConstructor(parameterTypes);
154-
155118
return cachedConstructor?.Invoke(parameters);
156119
}
157120

@@ -163,10 +126,6 @@ public CachedConstructor[] GetCachedConstructors()
163126
Type[] parameterTypes = parameters.ToTypes();
164127

165128
CachedConstructor? cachedConstructor = GetCachedConstructor(parameterTypes);
166-
167-
if (cachedConstructor == null)
168-
return default;
169-
170-
return cachedConstructor.Invoke<T>(parameters);
129+
return cachedConstructor is null ? default : cachedConstructor.Invoke<T>(parameters);
171130
}
172131
}

src/Fields/CachedFields.cs

Lines changed: 45 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
using System;
2+
using System.Collections.Frozen;
23
using System.Collections.Generic;
34
using System.Reflection;
4-
using Soenneker.Reflection.Cache.Extensions;
5+
using System.Runtime.CompilerServices;
56
using Soenneker.Reflection.Cache.Fields.Abstract;
67
using Soenneker.Reflection.Cache.Types;
78

@@ -10,95 +11,73 @@ namespace Soenneker.Reflection.Cache.Fields;
1011
///<inheritdoc cref="ICachedFields"/>
1112
public sealed class CachedFields : ICachedFields
1213
{
13-
private readonly Lazy<Dictionary<int, CachedField?>> _cachedDict;
14-
private readonly Lazy<CachedField[]> _cachedArray;
15-
16-
private readonly Lazy<FieldInfo[]> _fieldsCache;
17-
1814
private readonly CachedType _cachedType;
19-
2015
private readonly CachedTypes _cachedTypes;
2116

22-
private readonly bool _threadSafe;
17+
private readonly Lazy<BuiltCache> _built; // single source of truth
2318

24-
public CachedFields(CachedType cachedType, CachedTypes cachedTypes, bool threadSafe = true)
19+
private sealed class BuiltCache
2520
{
26-
_cachedType = cachedType;
27-
_cachedTypes = cachedTypes;
28-
_threadSafe = threadSafe;
21+
public readonly CachedField[] CachedArray;
22+
public readonly FrozenDictionary<string, CachedField> MapByName;
23+
public readonly FieldInfo[] FieldInfos;
2924

30-
_cachedDict = new Lazy<Dictionary<int, CachedField?>>(SetDict, threadSafe);
31-
_cachedArray = new Lazy<CachedField[]>(SetArray, threadSafe);
32-
_fieldsCache = new Lazy<FieldInfo[]>(() => GetCachedFields().ToFieldInfos(), threadSafe);
25+
public BuiltCache(CachedField[] cachedArray, FrozenDictionary<string, CachedField> mapByName, FieldInfo[] fieldInfos)
26+
{
27+
CachedArray = cachedArray;
28+
MapByName = mapByName;
29+
FieldInfos = fieldInfos;
30+
}
3331
}
3432

35-
public FieldInfo? GetField(string name)
33+
public CachedFields(CachedType cachedType, CachedTypes cachedTypes, bool threadSafe = true)
3634
{
37-
CachedField? cachedField = GetCachedField(name);
38-
return cachedField?.FieldInfo;
39-
}
35+
_cachedType = cachedType ?? throw new ArgumentNullException(nameof(cachedType));
36+
_cachedTypes = cachedTypes ?? throw new ArgumentNullException(nameof(cachedTypes));
4037

41-
public CachedField? GetCachedField(string name)
42-
{
43-
return _cachedDict.Value.GetValueOrDefault(name.GetHashCode());
38+
_built = new Lazy<BuiltCache>(BuildAll, threadSafe);
4439
}
4540

46-
private Dictionary<int, CachedField?> SetDict()
41+
private BuiltCache BuildAll()
4742
{
48-
var dict = new Dictionary<int, CachedField?>();
43+
// One reflection hit + one allocation of FieldInfo[]
44+
FieldInfo[] fields = _cachedType.Type!.GetFields(_cachedTypes.Options.FieldFlags);
45+
int len = fields.Length;
4946

50-
// If the array is already populated, build the dictionary from the array
51-
if (_cachedArray.IsValueCreated)
52-
{
53-
int length = _cachedArray.Value.Length;
47+
var cached = new CachedField[len];
48+
var dict = new Dictionary<string, CachedField>(len, StringComparer.Ordinal);
5449

55-
for (var index = 0; index < length; index++)
56-
{
57-
CachedField cachedField = _cachedArray.Value[index];
58-
dict[cachedField.FieldInfo.Name.GetHashCode()] = cachedField;
59-
}
60-
}
61-
else
50+
for (var i = 0; i < len; i++)
6251
{
63-
// If the array is not populated, build the dictionary directly
64-
FieldInfo[] fields = _cachedType.Type!.GetFields(_cachedTypes.Options.FieldFlags);
65-
66-
int length = fields.Length;
67-
68-
for (var i = 0; i < length; i++)
69-
{
70-
FieldInfo field = fields[i];
71-
var cachedField = new CachedField(field, _cachedTypes, _threadSafe);
72-
dict[field.Name.GetHashCode()] = cachedField;
73-
}
52+
FieldInfo fi = fields[i];
53+
var cf = new CachedField(fi, _cachedTypes, threadSafe: true); // thread safety here matches previous behavior
54+
cached[i] = cf;
55+
dict[fi.Name] = cf; // field names are unique per type for given BindingFlags
7456
}
7557

76-
return dict;
58+
return new BuiltCache(
59+
cached,
60+
dict.ToFrozenDictionary(StringComparer.Ordinal),
61+
fields // keep original FieldInfo[] so we don't re-convert later
62+
);
7763
}
7864

79-
private CachedField[] SetArray()
80-
{
81-
if (_cachedDict.IsValueCreated)
82-
{
83-
Dictionary<int, CachedField?>.ValueCollection values = _cachedDict.Value.Values;
84-
int count = values.Count;
85-
var result = new CachedField[count];
86-
values.CopyTo(result, 0);
87-
return result;
88-
}
89-
90-
FieldInfo[] fields = _cachedType.Type!.GetFields(_cachedTypes.Options.FieldFlags);
65+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
66+
public FieldInfo? GetField(string name)
67+
=> GetCachedField(name)?.FieldInfo;
9168

92-
return fields.ToCachedFields(_cachedTypes, _threadSafe);
69+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
70+
public CachedField? GetCachedField(string name)
71+
{
72+
// Fast, allocation-free lookup
73+
return _built.Value.MapByName.GetValueOrDefault(name);
9374
}
9475

76+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
9577
public FieldInfo[] GetFields()
96-
{
97-
return _fieldsCache.Value;
98-
}
78+
=> _built.Value.FieldInfos;
9979

80+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
10081
public CachedField[] GetCachedFields()
101-
{
102-
return _cachedArray.Value;
103-
}
82+
=> _built.Value.CachedArray;
10483
}

0 commit comments

Comments
 (0)