Dynamic Objects And Alternatives To Reflection

Introduction

The CodexMicroORM open source project on GitHub includes several features to help you create fast, concise .NET deliverables. One such feature is implemented in the Performance.cs file and enables dynamic (i.e. run-time) access to properties of any object – faster than what you’d get out of System.Reflection.Type. CodexMicroORM leverages this feature in several places, one being the ability to build composite objects: your existing “POCO” (plain-old C# objects), with additional optional extended properties.

Motivation

If we want to dynamically get property values from objects at run-time, there are a few approaches with varying performance overhead. It’s common knowledge that many methods in System.Reflection although powerful and easy to use, tend to be slow. An alternative described by Jon Skeet nearly 10 years ago is still valid: we can create delegates that wrap the “property get” (and set) methods - and cache these delegates. This approach is used in CodexMicroORM where we can use the following “Fast” extension methods on any “object”:

  • FastGetValue
  • FastSetValue
  • FastPropertyReadable
  • FastPropertyReadableWithValue
  • FastPropertyWriteable
  • FastGetAllProperties

If we're not building an app, site or service that requires ultra-high performance, traditional reflection may be "good enough" - but it's also good to understand the magnitude of performance differences involved.

Performance of Alternatives

Let’s compare the implementation and performance of a few approaches using a test harness:

  1. class Person {  
  2.     public string Name {  
  3.         get;  
  4.         set;  
  5.     }  
  6.     public int ? Age {  
  7.         get;  
  8.         set;  
  9.     }  
  10. }  
  11. static void Main(string[] args) {  
  12.     int iterations = 5000000;  
  13.     Person p = new Person() {  
  14.         Name = "Frank", Age = 30  
  15.     };  
  16.     int ? age = 0;  
  17.     Stopwatch sw = new Stopwatch();  
  18.     sw.Start();  
  19.     for (int i = 1; i <= iterations; ++i) {  
  20.         age += p.Age;  
  21.     }  
  22.     sw.Stop();  
  23.     Console.WriteLine($ "{sw.Elapsed.TotalMilliseconds * 1000.0 / iterations} microseconds per iteration (direct)");  
  24.     sw.Restart();  
  25.     Func < object, int ? > fn1 = (object o) => ((Person) o).Age;  
  26.     for (int i = 1; i <= iterations; ++i) {  
  27.         age += fn1(p);  
  28.     }  
  29.     sw.Stop();  
  30.     Console.WriteLine($ "{sw.Elapsed.TotalMilliseconds * 1000.0 / iterations} microseconds per iteration (strongly-typed delegate)");  
  31.     sw.Restart();  
  32.     Func < object, object > fn2 = (object o) => ((Person) o).Age;  
  33.     for (int i = 1; i <= iterations; ++i) {  
  34.         age += (int ? ) fn2(p);  
  35.     }  
  36.     sw.Stop();  
  37.     Console.WriteLine($ "{sw.Elapsed.TotalMilliseconds * 1000.0 / iterations} microseconds per iteration (delegate with boxing)");  
  38.     sw.Restart();  
  39.     for (int i = 1; i <= iterations; ++i) {  
  40.         age += (int ? ) p.GetType().GetProperty("Age").GetGetMethod().Invoke(p, new object[] {});  
  41.     }  
  42.     sw.Stop();  
  43.     Console.WriteLine($ "{sw.Elapsed.TotalMilliseconds * 1000.0 / iterations} microseconds per iteration (reflection)");  
  44.     sw.Restart();  
  45.     for (int i = 1; i <= iterations; ++i) {  
  46.         age += (int ? ) p.GetValueReflection("Age");  
  47.     }  
  48.     sw.Stop();  
  49.     Console.WriteLine($ "{sw.Elapsed.TotalMilliseconds * 1000.0 / iterations} microseconds per iteration (cached reflection)");  
  50.     sw.Restart();  
  51.     for (int i = 1; i <= iterations; ++i) {  
  52.         age += (int ? ) p.FastGetValue("Age");  
  53.     }  
  54.     sw.Stop();  
  55.     Console.WriteLine($ "{sw.Elapsed.TotalMilliseconds * 1000.0 / iterations} microseconds per iteration (CEF)");  
  56.     Console.ReadLine();  
  57. }  

On my laptop, I see around 12 nanoseconds for direct access to the Age property, a baseline for strongly-typed access. If I use a strongly typed delegate instead – where we know the return type – the speed is close: 18 ns. The problem with this is in “generic situations” where you don’t know the types of every property ahead of time, a more appropriate delegate type we could use (for caching) is “Func<object, object>”. We could be paying a performance penalty for boxing in this case, but that’s largely necessary for the flexibility gained. The question becomes “is the boxing penalty as large as that of reflection?” In my test harness, it’s 170 ns. Testing with the Name property instead, the performance is much better: basically, the same as the strongly-typed delegate since no boxing is required with a string. This becomes our baseline for the best we could possibly achieve using a delegate wrapper.

Using System.Reflection (GetProperty and related methods), I get 482 ns. This is a full 40 times slower than direct access and is what most people would get without considering an alternative. The final figure comes from CodexMicroORM’s FastGetValue method. This takes 312 ns: better than System.Reflection by 35% but not as good as access using an in-hand delegate.

This might be the end of the story, with a delegate beating reflection by a factor of nearly three - but acquiring the delegate in question is quite costly. The first set of statements to create the delegate use standard reflection, with the idea being you'd want to cache the resulting delegate for subsequent requests for the same property on the same type. This leads to the extra 140+ ns of overhead for caching itself.

Also, trying to cache the MethodInfo returned by GetGetMethod isn't a great option to improve reflection’s performance. Plumbing in equivalent caching compared to what CodexMicroORM uses currently, I get 573 ns per iteration, which is worse than the non-cached reflection calls in the above benchmark.

CodexMicroORM Implementation

As implemented prior to release 0.5.3, FastGetValue in the above benchmark was slower than reflection. It turns out the caching implementation matters quite a bit! Changes made in 0.5.3 include -

  • Use Dictionary vs. ConcurrentDictionary. Use reader/writer locks to protect the Dictionary for thread-safe access. A design goal of CodexMicroORM is thread-safety, supporting use of Parallel extensions within your apps to improve performance - or at least offer the option which is more than frameworks like Entity Framework can say. Incidentally, locking to support thread-safety accounts for about 70 ns of the reported overhead (half of the 140 ns), which led me to introduce “NoLock” versions of these methods, if you know for a fact you won’t have concurrency issues.
  • Instead of building a string dictionary key that includes type name and property name, I introduced a new struct (DelegateCacheKey) that doesn't require string concatenation. This supports a more efficient dictionary lookup.

A Practical Use

Our sample Person class above only has a name and age - so if we wanted to use it with most ORM frameworks (e.g. Entity Framework, Hibernate, etc.), you might set the primary key to be the name. That might work - if you're willing to have names be unique, but it's common to instead use a surrogate key, such as an integer PersonID property, to uniquely identify each Person.

CodexMicroORM offers a way to do this without altering your business class. The DynamicWithBag.cs class relies on the "Fast" methods to move data to and from your CLR (business) objects. For properties that are not on the CLR objects, a backing dictionary is used. What you end up with is your business objects are usable as expected (direct, extremely fast non-dynamic property access), and extended properties are accessible using syntax such as this,

  1. // Would typically stick this in AppDomain level initialization code since needs to run only once per life of AppDomain    
  2. KeyService.RegisterKey<Person>("PersonID");    
  3.     
  4. // NewObject hands back a “tracked” Person    
  5. var p = CEF.NewObject<Person>(new Person() { Name = "Fred", Age = 30 });    
  6.     
  7. // Assuming we'd done some basic initial setup, this would insert a record in a database    
  8. p.DBSave();    
  9.     
  10. // The database save has assigned our PersonID for this new record, based on our RegisterKey statement - PersonID does not exist on the Person class    
  11. Assert.IsTrue(p.AsDynamic().PersonID > 0);    
  12.     
  13. // This works (and is fast - it's not dynamic) - but our class does not see a property change so the entity does not look dirty    
  14. p.Age = 31;    
  15.     
  16. // This also works - and causes the object to appear "changed" to CodexMicroORM, through use of a dynamic wrapper    
  17. p.AsDynamic().Age = 32;    
  18.     
  19. // Age is 32, as expected, on our business object    
  20. Assert.AreEqual(32, p.Age);    
  21.     
  22. // Fred's age is now 32 in the database as well (update fires since looked changed - now is "clean")   
  23. p.DBSave();  
Notice here we can mix direct property access and extended property access. "AsDynamic" hands back a DLR object that has not just the ability to use these extra properties but tracks their changes (using INotifyPropertyChanged). This, in turn, grants functionality such as dirty state tracking, even if your business objects don't implement INotifyPropertyChanged. It also means you can use ORM functionality with pretty much any object model – you don’t need to generate one bound directly to your database.

Implementing changes in delegate caching causes some pre-existing CodexMicroORM benchmarks to perform even better, by a few percents. Your real benefits depend on your workload - but choosing the best approach as I've shown is easy. In fact, you can add a NuGet reference to CodexMicroORM:

CodexMicroORM

... and now, you've got access to the "Fast" methods, using the CodexMicroORM.Core.Helper namespace.

On GitHub, you can find more working examples - and feel free to contribute!

X

Build smarter apps with Machine Learning, Bots, Cognitive Services - Start free.

Start Learning Now