Continuing with the performance analysis of Retina.NET and Reflection, I have found some interesting results.
In the previous post we see that in order to retrieve an entity from storage we must basically perform three operations: execute the DataReader, create the instance of the class being retrieved and populate the created instance with the DataReader. This operations consume the 42.6%, 15.4% and 20.2% respectively.
In this new test I focused on trying to minimize the costs of using Reflection, and therefore implemented some new features in Retina.NET. The first is the use of an entity factory class to minimize the instance build time avoiding the Activator.CreateInstance call, and the second new feature is the use of an field accesor interface in order to minimize the PropertyInfo.GetValue / SetValue usage.
Using a profiler I obtained the below results, where the execution tree now shows the changes:
Retina v220.127.116.11 - Pass #1 Inclusive Exclusive InternalDataStoreBroker.Retrieve 100% 0.2% Entity.ResetIsDirty 7.3% 0.5% BaseEntityPersister.Retrieve 91.2% 0.7% IDbCommand.ExecuteReader 47.3% 0.1% UserFactory.Build 10.7% 0.6% BaseEntityPersister.RetrieveChildEntities 10.7% 5.2% EntityStorageDefn.PopulateEntity 20.6% 6.4% SqlDataReader.get_Item 40.5% 4.1% EntityMember.SetValue 43.0% 8.3% Reflection.FieldInfo.SetValue 79.1% 1.5% User.SetValue 12.6% 50%
Retina v18.104.22.168 - Pass #1
The test was run 15 times on the same machine as before (P4 2.0Gz, 1Gb Ram) and the new average execution time for retrieving 1000 entities was 1402.12ms (1.40 for each entity, a 9% less than the previous run).
Let's analyze the execution tree. I marked in bold the entries that deserve more attention, so move on to explain a little more of what is going on:
- Of the total retrieve time 7.3% is spent in the "Entity.ResetIsDirty" method (down from 9.5%). I have made some tunning on this method and it shows, but still I have the feeling that this is too high.
- Inside the "BaseEntityPersister.Retrieve" method we see that 47.3% of the time is spent in executing the reader. It was 42.6% in the previous test and the higher number is better because it means that we are spending less time in other things.
- The creation of the entity by using a factory class now is consuming 10.7% of the retrieve time. The use of the factory class eliminates all reflection usage and cannot be reduced any further.
- The population of the entity now consumes 20.6% of the total retrieve time (similar to the previous 20.2%), and fecthing of data from the reader now consumes 40.5% of the entity population time (down from 47.1%) and that means that we are doing more work setting the entity values than before.
- Of the total entity population time, the 43% now is used in setting the entity values (a little up from 40.3%), and from that the 79.1% still corresponds to the use of Reflection used to set the read-ony fields.
The use of Reflection for setting read-only members still bothers me, but I can't find a solution for this dilemma: performance or data integrity. So, for now I choose data integrity. I have made some calculations and reach the conclusion that if I eliminate Reflection totally for setting the entity PK I would save aprox. 100ms for every 1000 retrieve operations made, witch means that each entity would load in 1.30ms instead of 1.40ms (a 7% less).
I feel that a 7% less in loading time does not justify the changes needed to eliminate Reflection for those cases, and Retina.NET will continue to use it until I found a better solution. Again, Whidbey can help in that direction.
We can point that viewing those numbers you can estimate that the 60% of the entity retrieval time is spent in executing and fetching the reader, and the remaining 40% is the overhead of using objects. From that, the 10% is consumed by the creation of the class instance. For that remaining 30%, 20% is used for populating the entity and the 10% is used populating child entities.
If you still follow me, you can conclude with me that the cost of using entity objects is aproximately 20% over using raw data readers, and that is really a low amount of overhead given the benefits.
From that you can also conclude that Retina.NET exclusive overhead is very little, and close to zero. My numbers indicates that the performance cost of using Retina.NET instead of a custom made entity and persistent code is lower than 5%. I'm very happy.
As always, any comments are welcome.
Andrés G Vettori
Leader of the C# Community of the Microsoft Users Group Argentina