This is an except from some notes I put together while working on peices of the entity object framework I developed a couple months back...
During the initial design of the Entity Object Framework a focus was placed on creating a set of base classes to handle the core CRUD (Create, Read, Update, Delete) operations for persistent objects.
The Reflective Approach
The initial implementation in the EOF used a combination of custom attributes, and reflection to persist objects. By looping over all the properties of an object and checking for a SqlParameter attribute the framework could successfully create a SqlDb object with all the parameters necessary for saving the object to the data store. (See ReflectiveSave.cs)
While this method of automatically executing the proper SQL required for the save of the object performed the desired actions it didn' t seem like an optimal solution (although not bad for starters).
After further analysis it became clear that the overhead associated with looping though all the properties of an object each time a save was performed was not going to cut it. A more streamlined approach that only involved looping over the properties on an initial save would undoubtedly result in better performance. The only question was how best to implement this approach& an xml file for lookups? ...some other caching mechanism? & or better yet how about a dynamic assembly.
The Dynamic Approach
The .NET Framework provides two main options when it comes to creating dynamic assemblies. The first option is to use the System.CodeDom classes to programmatically create code constructs, save the results to disk, invoke a compiler, and then dynamically load the assembly into memory. The second more direct approach is to use the System.Reflection.Emit classes to create a dynamic assembly using Intermediate Language.
CodeDom -
The System.CodeDom Namespace provides several classes for creating C# or VB.NET source code. Using theses classes a developer has the ability to dynamically create a source file. Once the source file is generated one can invoke the necessary compiler to create an assembly from the source file(s). After generating an assembly from the dynamically created source files the assembly can be loaded using Assembly.Load. The advantages to this method are:
- The generated source file can be viewed
- Debugging is easier
- The method of generating the source file is more straight forward then other methods
The disadvantages of this method are:
Dynamically generated source files may crowd up the directory
The performance isn't as good as creating the assembly in memory using IL (based on research no actual test performed)
- Update: These are both invalid points, not sure where they came from :-)
Reflection.Emit -
The compilation of C#, VB.NET, Jscript.NET, Cobol.NET or any other language in the .NET framework results in the creation of Intermediate Language (IL). When the application is executed the Just-in-Time (JIT) compiler converts the IL into machine code. By dynamically creating an assembly in IL using the System.Reflection.Emit class we can create an in memory assembly for our intended need. The advantages to this method are:
- The generated assembly can be optimized using specific IL instructions
- The performance of the dynamic assembly is VERY GOOD
The disadvantages to this method are:
- Debugging the dynamic assembly is very difficult
- There is limited documentation regarding the creation of dynamic assemblies using Reflection.Emit
To to create the dynamic assemblies for the EOF I chose to use the Reflection.Emit method. The process of creating an assembly using this method required me to get familiar with IL, as well as the process for creating methods in IL.
Creating the IL
The C#, VB.NET, Cobol.NET, and all other compilers that support the .NET Framework generate IL when compiling a set of code. To get an idea of what IL looks like, start up ILDASM by opening a Visual Studio .NET Command Prompt (Start -> All Programs -> Microsoft Visual Studio .NET -> Visual Studio .NET Tools -> Visual Studio .NET Command Prompt) and type ILDASM. The ILDASM tool is an IL Disassembler for the .NET framework which is installed as part of the .NET SDK. To view the IL for any .NET component on your system go to File -> Open and select a .NET assembly.
To get familiar with IL I wrote a simple assembly in C# that performed a set of operations. The operations were modeled on the method design I had in my mind for the dynamic assembly. Looking at the resultant IL allowed me to get a better understanding of the ways in which IL Op Codes where used, as well as how I would need to chain them together to get the desired result.
After getting fairly familiar with the IL that would be necessary for my objects I began the construction of my DynamicAssemblyManager class. The DynamicAssemblyManager class needed to have the ability to create a dynamic assembly for saving objects, as well as have the ability to store the cached assemblies in an easy lookup. To handle the creation of dynamic assemblies a CreateAssembly method was created. The CreateAssembly method accepted an object and returned a dynamically generated assembly that could save any object of the type passed in. A private hash table was added to the DynamicAssemblyManager class to allow a quick and easy lookup of already generated assemblies.
The final piece necessary to complete the implementation of the dynamic assembly persistence was in the entity object class. A private DynamicAssemblySave method was added to the class which handles the process of checking the DynamicAssemblyManager class for the existence of a dynamic assembly, and if it doesn't exist, creating it.
See CreateAssembly.cs
See DynamicAssemblySave.cs
The Results
After all the coding was done it was finally time to test the different methods for persisting the object to the database. In total we ended up with three types of methods for saving the properties of an object to the database.
Method #1:
Use reflection to loop through all the properties of an object and dynamically add the necessary parameters and parameter values to the SqlDb object to be saved. (See ReflectiveSave.cs)
Method #2:
Dynamically generate an assembly to handle the persistence of the object to the database. (See CreateAssembly.cs and DynamicAssemblySave.cs)
Method #3:
Write a custom method to save the object . (See CustomSave.cs)
To test the performance of each of the methods specified above a NUnit test was setup. The test class looped 10,000/100,000/1,000,000 times and called each of the 3 save methods. In order to test only the performance of the object operations and not the actual execution of the SQL call against the database the objects used a stub SqlDb object.
The results of the performance tests are detailed below:
First set of 10,000 repetitions
- Save w/ Reflection: 2094 ms
- Dynamic Assembly: 296 ms
- Custom Save: 266 ms
Second Set of 10,000 repetitions
- Save w/ Reflection: 1953 ms
- Dynamic Assembly: 281 ms
- Custom Save: 266 ms
First set of 100,000 repetitions
- Save w/ Reflection: 19,016 ms
- Dynamic Assembly: 2,625 ms
- Custom Save: 2,438 ms
Second Set of 100,000 repetitions
- Save w/ Reflection: 18,672
- Dynamic Assembly: 2,641
- Custom Save: 2,390
First set of 1,000,000 repetitions
- Save w/ Reflection: 170,047 ms
- Dynamic Assembly: 21,688 ms
- Custom Save: 20,187 ms
Second Set of 1,000,000 repetitions
- Save w/ Reflection: BOOM!
- Dynamic Assembly: 21,687
- Custom Save: 20,657
As you can see from the performance tests above the dynamic assembly performed almost as well as the custom written save method. The slight difference may be due to the generation of the assembly or due to casting involved within the dynamic assembly. Some tweaks to the IL may help make this difference smaller or non existent.
References:
http://msdn.microsoft.com/library/default.asp?url=/library/en-us/cpref/html/frlrfSystemReflectionEmit.asp
http://msdn.microsoft.com/library/default.asp?url=/library/en-us/cpref/html/frlrfsystemreflectionemitopcodesclasstopic.asp
http://msdn.microsoft.com/library/default.asp?url=/library/en-us/cpref/html/frlrfSystemCodeDom.asp