|
| 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 | + |
| 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) |
0 commit comments