Skip to content

Commit 93d8a62

Browse files
committed
New Create instance from type post
1 parent 9e0bbbe commit 93d8a62

File tree

4 files changed

+370
-2
lines changed

4 files changed

+370
-2
lines changed

_posts/2012-01-11-csharp-expression-tree-create-instance-from-type-extension-method.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,7 @@ tags: [Expression Trees, Performance]
77

88
<span class="updated">
99
Edit: I've now written an improved version of this set of extension methods, which can be found
10-
[here](fast-csharp-expression-tree-create-instance-from-type-extension-method). Consider these
11-
deprecated!
10+
[here](create-instance-of-type-net-core). Consider these deprecated!
1211
</span>
1312

1413
<span class="first">

_posts/2012-02-19-fast-csharp-expression-tree-create-instance-from-type-extension-method.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,17 @@ tags: [Expression Trees, Performance]
66
images_dir: '2012-02-19/'
77
---
88

9+
<span class="updated">
10+
Edit: I've now written an improved version of this set of extension methods, which can be found
11+
[here](create-instance-of-type-net-core). Consider these deprecated!
12+
</span>
13+
14+
<span class="first">
915
Having written [an extension method](csharp-expression-tree-create-instance-from-type-extension-method)
1016
to create an instance from a Type and been
1117
[a bit underwhelmed](csharp-performance-new-expression-tree-func-activator-createinstance) by its
1218
performance, I looked into exactly what was happening and have now got it working much, _much_ faster.
19+
</span>
1320

1421
To recap, the problem with the first version of this method is that it cached the `Func`s it created
1522
with all their argument types as `object`, which meant they had to be cast every time the `Func` was
Lines changed: 362 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,362 @@
1+
---
2+
layout: post
3+
title: Creating an Instance of Type, 8 Years Later
4+
excerpt: I recently had an enquiry about code in a post from 2012, and thought I'd revisit it. It's better now.
5+
tags: [Expression Trees, Performance]
6+
images_dir: '2020-07-11/'
7+
---
8+
9+
Back in 2012, I [wrote about](fast-csharp-expression-tree-create-instance-from-type-extension-method)
10+
using Expression Trees to create a Func which creates an instance of a type at runtime. Having recently
11+
had an enquiry regarding that code, I thought I'd revisit it to see if I can do better now.
12+
13+
<span class="updated">
14+
_tl;dr_: [yes, I can](#performance).
15+
</span>
16+
17+
## 2012
18+
19+
Back in the day, I wrote this (for a more complete description, see
20+
[the blog]((https://github.com/agileobjects/eg-create-instance-from-type/blob/master/CreateInstanceFromType/CreateInstanceFromType2012.cs))):
21+
22+
23+
```csharp
24+
// To allow for overloads with differing numbers of
25+
// arguments, flag arguments to ignore using this type:
26+
private class TypeToIgnore
27+
{
28+
}
29+
30+
public static object GetInstance(this Type type)
31+
{
32+
return GetInstance<TypeToIgnore>(type, null);
33+
}
34+
35+
public static object GetInstance<TArg>(
36+
this Type type,
37+
TArg argument)
38+
{
39+
return GetInstance<TArg, TypeToIgnore>(type, argument, null);
40+
}
41+
42+
public static object GetInstance<TArg1, TArg2>(
43+
this Type type,
44+
TArg1 argument1,
45+
TArg2 argument2)
46+
{
47+
return GetInstance<TArg1, TArg2, TypeToIgnore>(
48+
type, argument1, argument2, null);
49+
}
50+
51+
public static object GetInstance<TArg1, TArg2, TArg3>(
52+
this Type type,
53+
TArg1 argument1,
54+
TArg2 argument2,
55+
TArg3 argument3)
56+
{
57+
return InstanceCreationFactory<TArg1, TArg2, TArg3>
58+
.CreateInstanceOf(type, argument1, argument2, argument3);
59+
}
60+
61+
private static class InstanceCreationFactory<TArg1, TArg2, TArg3>
62+
{
63+
// A dictionary of object-creation Funcs, keyed by the created type:
64+
private static readonly Dictionary<Type, Func<TArg1, TArg2, TArg3, object>>
65+
_instanceCreationMethods =
66+
new Dictionary<Type, Func<TArg1, TArg2, TArg3, object>>();
67+
68+
public static object CreateInstanceOf(
69+
Type type,
70+
TArg1 arg1,
71+
TArg2 arg2,
72+
TArg3 arg3)
73+
{
74+
CacheInstanceCreationMethodIfRequired(type);
75+
76+
return _instanceCreationMethods[type]
77+
.Invoke(arg1, arg2, arg3);
78+
}
79+
80+
private static void CacheInstanceCreationMethodIfRequired(
81+
Type type)
82+
{
83+
// Bail out if we've already cached the instance
84+
// creation method:
85+
if (_instanceCreationMethods.ContainsKey(type))
86+
{
87+
return;
88+
}
89+
90+
var argumentTypes = new[]
91+
{
92+
typeof(TArg1), typeof(TArg2), typeof(TArg3)
93+
};
94+
95+
// A collection of the constructor argument Types we've
96+
// been given; ignore any of the 'ignore this' Type:
97+
Type[] constructorArgumentTypes = argumentTypes
98+
.Where(t => t != typeof(TypeToIgnore))
99+
.ToArray();
100+
101+
// The constructor which matches the given argument types:
102+
var constructor = type.GetConstructor(
103+
BindingFlags.Instance | BindingFlags.Public,
104+
null,
105+
CallingConventions.HasThis,
106+
constructorArgumentTypes,
107+
new ParameterModifier[0]);
108+
109+
// A set of Expressions representing the parameters to
110+
// pass to the Func:
111+
var lamdaParameterExpressions = new[]
112+
{
113+
Expression.Parameter(typeof(TArg1), "param1"),
114+
Expression.Parameter(typeof(TArg2), "param2"),
115+
Expression.Parameter(typeof(TArg3), "param3")
116+
};
117+
118+
// A set of Expressions representing the parameters to
119+
// pass to the constructor:
120+
var constructorParameterExpressions =
121+
lamdaParameterExpressions
122+
.Take(constructorArgumentTypes.Length)
123+
.ToArray();
124+
125+
// An Expression representing the constructor call,
126+
// passing in the constructor parameters:
127+
var constructorCallExpression = Expression
128+
.New(constructor, constructorParameterExpressions);
129+
130+
// Compile the Expression into a Func which takes three
131+
// arguments and returns the constructed object:
132+
var constructorCallingLambda = Expression
133+
.Lambda<Func<TArg1, TArg2, TArg3, object>>(
134+
constructorCallExpression,
135+
lamdaParameterExpressions)
136+
.Compile();
137+
138+
// Store the compiled Func in the cache Dictionary:
139+
_instanceCreationMethods[type] = constructorCallingLambda;
140+
}
141+
```
142+
143+
#### Performance Notes
144+
145+
- `CreateInstanceOf` calls `CacheInstanceCreationMethodIfRequired` to ensure the required
146+
Func exists, then retrieves that Func from the `Dictionary`. This is two lookups for every call,
147+
which is one more than necessary. I don't think I knew about
148+
[`Dictionary.TryGetValue`](https://docs.microsoft.com/en-us/dotnet/api/system.collections.generic.dictionary-2.trygetvalue?view=netframework-2.0)
149+
back then.
150+
151+
- `CacheInstanceCreationMethodIfRequired` creates three-element `argumentTypes` and
152+
`lamdaParameterExpressions` arrays, then uses Linq to create new arrays with only the required
153+
elements. That's just plain inefficient.
154+
155+
#### Code Notes
156+
157+
- The cache `Dictionary` is not thread-safe - all threads would create the same Func, and assigning by
158+
index instead of calling `Add()` avoids
159+
[item with the same key has already been added](https://stackoverflow.com/questions/26516825/argument-exception-item-with-same-key-has-already-been-added/26517435),
160+
but I'm not convinced `Dictionary` itself couldn't get into an internal mess.
161+
162+
Still, it was faster than [`Activator.CreateInstance`](https://docs.microsoft.com/en-us/dotnet/api/system.activator.createinstance),
163+
so that's good... right...? Well...
164+
165+
### Design-time vs Runtime
166+
167+
In my original blog, I compared these methods against `Activator.CreateInstance`, but that's not
168+
really a fair comparison. `Activator` can't get the constructor argument types from generic method
169+
arguments - it takes an `object` array, and has to get them from its elements. In other words, it
170+
gets them at runtime, where my methods are given them at
171+
[design-time](https://stackoverflow.com/questions/2621976/run-time-vs-design-time).
172+
173+
There's a couple of problems with this:
174+
175+
- It's not an apples-to-apples comparison
176+
177+
- If you need to create an instance of a `Type` object at _runtime_, you might not have its constructor
178+
argument types at _design_-time
179+
180+
So let's start over.
181+
182+
## 2020
183+
184+
I've rewritten the
185+
[design-time version](https://github.com/agileobjects/eg-create-instance-from-type/blob/master/CreateInstanceFromType/CreateInstanceFromType2020DesignTimeArgs.cs),
186+
and added a [runtime version](https://github.com/agileobjects/eg-create-instance-from-type/blob/master/CreateInstanceFromType/CreateInstanceFromType2020RuntimeArgs.cs).
187+
188+
### Design-time
189+
190+
Here's the two-argument version of my updated Design-time methods:
191+
192+
```csharp
193+
public static object GetInstance<TArg1, TArg2>(
194+
this Type type,
195+
TArg1 argument1,
196+
TArg2 argument2)
197+
{
198+
return InstanceFactoryCache<TArg1, TArg2>.GetFactoryFor(type)
199+
.Invoke(argument1, argument2);
200+
}
201+
202+
private static class InstanceFactoryCache<TArg1, TArg2>
203+
{
204+
// A dictionary of object-creation Funcs, keyed by the created type:
205+
private static readonly ConcurrentDictionary<Type, Func<TArg1, TArg2, object>>
206+
_factoriesByType =
207+
new ConcurrentDictionary<Type, Func<TArg1, TArg2, object>>();
208+
209+
public static Func<TArg1, TArg2, object> GetFactoryFor(Type type)
210+
{
211+
return _factoriesByType.GetOrAdd(type, t =>
212+
{
213+
// The argument types:
214+
var arg1Type = typeof(TArg1);
215+
var arg2Type = typeof(TArg2);
216+
217+
// The matching constructor:
218+
var ctor = t.GetConstructor(new[] { arg1Type, arg2Type });
219+
220+
// A set of Expressions representing the parameters to
221+
// pass to the Func and constructor:
222+
var argument1 = Expression.Parameter(arg1Type, "param1");
223+
var argument2 = Expression.Parameter(arg2Type, "param2");
224+
225+
// An Expression representing the constructor call,
226+
// passing in the constructor parameters:
227+
var instanceCreation = Expression
228+
.New(ctor, argument1, argument2);
229+
230+
// Compile the Expression into a Func which takes two
231+
// arguments and returns the constructed object:
232+
var instanceCreationLambda = Expression
233+
.Lambda<Func<TArg1, TArg2, object>>(
234+
instanceCreation, argument1, argument2);
235+
236+
return instanceCreationLambda.Compile();
237+
});
238+
}
239+
}
240+
```
241+
242+
To note:
243+
244+
- Instead of using `TypeToIgnore`, each method (parameterless, 1-parameter, 2-parameters, etc) now
245+
has its own cache. This leads to some code duplication, but the focus here is on _performance_, so
246+
[DRY](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself) is less important than not creating
247+
objects unnecessarily.
248+
249+
- I'm now using a [`ConcurrentDictionary`](https://docs.microsoft.com/en-us/dotnet/api/system.collections.concurrent.concurrentdictionary-2)
250+
for thread-safety, with [`GetOrAdd()`](https://docs.microsoft.com/en-us/dotnet/api/system.collections.concurrent.concurrentdictionary-2.getoradd)
251+
ensuring a single lookup is performed.
252+
253+
### Runtime
254+
255+
The [runtime-arguments code](https://github.com/agileobjects/eg-create-instance-from-type/blob/master/CreateInstanceFromType/CreateInstanceFromType2020RuntimeArgs.cs)
256+
stores Funcs in a `ConcurrentDictionary`, keyed by a custom key class (abbreviated code shown):
257+
258+
```csharp
259+
private class TypeFactoryKey
260+
{
261+
private readonly int _hashCode;
262+
263+
public TypeFactoryKey(Type type, object[] arguments)
264+
{
265+
Type = type;
266+
_hashCode = type.GetHashCode();
267+
268+
var argumentCount = arguments.Length;
269+
270+
unchecked
271+
{
272+
switch (argumentCount)
273+
{
274+
case 0:
275+
ArgumentTypes = Type.EmptyTypes;
276+
return;
277+
278+
case 1:
279+
{
280+
var argument = arguments[0];
281+
var argumentType = argument.GetType();
282+
ArgumentTypes = new[] { argumentType };
283+
_hashCode = GetHashCodeValue(argumentType);
284+
return;
285+
}
286+
287+
default:
288+
ArgumentTypes = new Type[argumentCount];
289+
290+
for (var i = 0; i < argumentCount; ++i)
291+
{
292+
var argument = arguments[i];
293+
var argumentType = argument.GetType();
294+
ArgumentTypes[i] = argumentType;
295+
_hashCode = GetHashCodeValue(argumentType);
296+
}
297+
298+
return;
299+
}
300+
}
301+
}
302+
303+
private int GetHashCodeValue(Type argumentType)
304+
=> (_hashCode * 397) ^ argumentType.GetHashCode();
305+
306+
public Type Type { get; }
307+
308+
public Type[] ArgumentTypes { get; }
309+
310+
public override bool Equals(object obj)
311+
=> ((TypeFactoryKey)obj)._hashCode == _hashCode;
312+
313+
public override int GetHashCode() => _hashCode;
314+
}
315+
```
316+
317+
To note:
318+
319+
- The key uses ReSharper's ['397' method](https://stackoverflow.com/questions/102742/why-is-397-used-for-resharper-gethashcode-override)
320+
to generate a [hash code](https://docs.microsoft.com/en-us/dotnet/api/system.object.gethashcode) for
321+
itself by combining the hash codes of the type to create, and any argument types. Through overriding
322+
`Equals` and `GetHashCode`, the `ConcurrentDictionary` uses this value to look up the appropriate
323+
Func. The parameterless and single-argument cases are optimised - the arguments array is iterated
324+
for other cases.
325+
326+
- The key stores all the information the Expression Tree code needs to create the Func - this avoids
327+
creating a [closure](https://www.simplethread.com/c-closures-explained) to call `GetOrAdd`.
328+
329+
- The [Expression Tree Func-creation](https://github.com/agileobjects/eg-create-instance-from-type/blob/master/CreateInstanceFromType/CreateInstanceFromType2020RuntimeArgs.cs#L27)
330+
is similar to the other examples.
331+
332+
- Null arguments [are handled](https://github.com/agileobjects/eg-create-instance-from-type/blob/master/CreateInstanceFromType.Tests/InstanceFromTypeCreationTests.cs#L68),
333+
but edge-cases exist where passing a null argument means the code won't be able to find a constructor
334+
to call; the same is true for `Activator`. So try not to do that.
335+
336+
## Performance
337+
338+
Ok, here's what we care about - how do the different methods perform? Here's some
339+
[Benchmark.NET](https://benchmarkdotnet.org) results, grouped by design-time vs runtime, and parameter
340+
count:
341+
342+
![Benchmark.NET Results]({{ site.post_images_dir }}{{ page.images_dir }}Results.png)
343+
344+
To note:
345+
346+
1. My 2020 design-time code is between **1.5** and **2.5** times **faster** than my 2012 code. Single
347+
cache lookups and strategic code duplication FTW.
348+
349+
2. My 2020 runtime code is between **6.7** and **6.9** times **faster** than `Activator.CreateInstance`
350+
and allocates way less memory, except for the parameterless case. `Activator.CreateInstance` is
351+
optimised for parameterless, but my method is only **1.3** times **slower** for parameterless
352+
constructors. I could always just delegate the parameterless case to `Activator`.
353+
354+
So there we go - fast object creation from a type with either design-time or runtime-typed arguments.
355+
It's been interesting to reimplement something I wrote 8 years ago, and see how my coding has changed
356+
in that time.
357+
358+
## Links
359+
360+
- 2020 [Design-time arguments]((https://github.com/agileobjects/eg-create-instance-from-type/blob/master/CreateInstanceFromType/CreateInstanceFromType2020DesignTimeArgs.cs)) code
361+
- 2020 [Runtime arguments]((https://github.com/agileobjects/eg-create-instance-from-type/blob/master/CreateInstanceFromType/CreateInstanceFromType2020RuntimeArgs.cs)) code
362+
- Sample code repo [on GitHub](https://github.com/agileobjects/eg-create-instance-from-type)
25.7 KB
Loading

0 commit comments

Comments
 (0)