Click here to Skip to main content
15,887,027 members
Please Sign up or sign in to vote.
4.33/5 (5 votes)
I wrote a simple object copier that copies public properties.
I can't figure out why the Dynamic method is a lot slower than the c# version.


Durations
C# method : 4,963 ms
Dynamic method : 19,924 ms


Full (console program) code:
C#
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using System.Text;
using System.Threading.Tasks;

namespace DuckCopy.SpeedTests
{
    class Program
    {
        const int NBRECORDS = 100 * 1000 * 1000;

        public class Person
        {
            private int mSomeNumber;

            public string FirstName { get; set; }
            public string LastName { get; set; }
            public DateTime DateOfBirth { get; set; }
            public int SomeNumber
            {
                get { return mSomeNumber; }
                set { mSomeNumber = value; }
            }
        }

        public static Action<T1, T2> CreateCopier<T1, T2>()
        {
            var meth = new DynamicMethod("copy", null, new Type[] { typeof(T1), typeof(T2) }, restrictedSkipVisibility: true);
            ILGenerator il = meth.GetILGenerator();
            int cpt = 0;

            var stopHere = typeof(Program).GetMethod("StopHere");

            foreach (var mi1 in typeof(T1).GetProperties(BindingFlags.Public | BindingFlags.Instance))
            {
                var mi2 = typeof(T2).GetProperty(mi1.Name, BindingFlags.Public | BindingFlags.Instance);
                if (mi1 != null && mi2 != null)
                {
                    cpt++;
                    il.Emit(OpCodes.Nop);
                    il.Emit(OpCodes.Ldarg_1);
                    il.Emit(OpCodes.Ldarg_0);
                    il.Emit(OpCodes.Callvirt, mi1.GetMethod);
                    il.Emit(OpCodes.Callvirt, mi2.SetMethod);
                }
            }
            il.Emit(OpCodes.Ret);

            var dlg = meth.CreateDelegate(typeof(Action<T1, T2>));
            return (Action<T1, T2>)dlg;
        }

        static void Main(string[] args)
        {
            var person1 = new Person() { FirstName = "Pascal", LastName = "Ganaye", DateOfBirth = new DateTime(1909, 5, 1), SomeNumber = 23456 };
            var person2 = new Person();

            var copyUsingAMethod = (Action<Person, Person>)CopyPerson;
            var copyUsingADynamicMethod = CreateCopier<Person, Person>();

            copyUsingAMethod(person1, person2); // 4882 ms
            var sw = Stopwatch.StartNew();
            for (int i = 0; i < NBRECORDS; i++)
            {
                copyUsingAMethod(person1, person2);
            }
            Console.WriteLine("{0} ms", sw.ElapsedMilliseconds);

            copyUsingADynamicMethod(person1, person2); // 19920 ms
            sw = Stopwatch.StartNew();
            for (int i = 0; i < NBRECORDS; i++)
            {
                copyUsingADynamicMethod(person1, person2); 
            }
            Console.WriteLine("{0} ms", sw.ElapsedMilliseconds);


            Console.ReadKey(intercept: true);
        }

        private static void CopyPerson(Person person1, Person person2)
        {
            person2.FirstName = person1.FirstName;
            person2.LastName = person1.LastName;
            person2.DateOfBirth = person1.DateOfBirth;
            person2.SomeNumber = person1.SomeNumber;
        }
    }
}


From what I can debug the two methods have the same IL code.

MSIL
IL_0000: nop        
IL_0001: ldarg.1    
IL_0002: ldarg.0    
IL_0003: callvirt   System.String get_FirstName()/DuckCopy.SpeedTests.Program+Person
IL_0008: callvirt   Void set_FirstName(System.String)/DuckCopy.SpeedTests.Program+Person
IL_000d: nop        
IL_000e: ldarg.1    
IL_000f: ldarg.0    
IL_0010: callvirt   System.String get_LastName()/DuckCopy.SpeedTests.Program+Person
IL_0015: callvirt   Void set_LastName(System.String)/DuckCopy.SpeedTests.Program+Person
IL_001a: nop        
IL_001b: ldarg.1    
IL_001c: ldarg.0    
IL_001d: callvirt   System.DateTime get_DateOfBirth()/DuckCopy.SpeedTests.Program+Person
IL_0022: callvirt   Void set_DateOfBirth(System.DateTime)/DuckCopy.SpeedTests.Program+Person
IL_0027: nop        
IL_0028: ldarg.1    
IL_0029: ldarg.0    
IL_002a: callvirt   Int32 get_SomeNumber()/DuckCopy.SpeedTests.Program+Person
IL_002f: callvirt   Void set_SomeNumber(Int32)/DuckCopy.SpeedTests.Program+Person
IL_0034: nop        
IL_0035: ret   
Posted
Comments
Sergey Alexandrovich Kryukov 16-Nov-12 22:31pm    
Come to thing about, pretty interesting question. And well explained, which unfortunately almost never happen these days. My 5, for sure.
--SA
Nissim Salomon 31-Aug-13 15:02pm    
Hi
I'm facing some issues with Dynamic Code Generation can you please tell me how did you Dump the DynamicMethod IL code ? what kind of tools & commands did you use ?

Hard to say exactly why the timing results are as you observe them, but I can see one problem: you time the calls of the method incorrectly, because you don't take into account the time taken by JIT. Again, both methods are eventually JIT-interpreted, so it's pretty hard to explain why the results are so different, but in all cases, you should first exclude JIT from equation.

You can assume, most conservatively, that a method is not yet JIT-compiled before it is called for them very first time. That said, you should wind up your stopwatch only when a method is called for the second time. In the most simplistic approach, you can just repeat the call of the method which performs all the timing twice — usually, you can see that the results of timing on the second run are different.

I'm not sure it will give you more explainable results, as could be some other problems, but this is something you can do first. I would be good it you report back your results.

In all my observations, using Dynamic Methods is very efficient. If the MSIL code is written well via the System.Reflection.Emit, and if the generation of methods (which is itself pretty slow) is properly reused and performed once.

—SA
 
Share this answer
 
Comments
Pascal Ganaye 17-Nov-12 5:34am    
I agree with you this is super fast.

I thought of the compilation phase.
In fact you can see in the code that I call copyUsingADynamicMethod once before starting the Stopwatch.
By the time I start the stopwatch, the code has already been compiled and run once.

I did find out why it is 4 time slower. It has to do with anonymous assemblies that triggers more control from .NET 4 framework. I wonder if they upload them somewhere for a quick NSA Check?
Sergey Alexandrovich Kryukov 17-Nov-12 21:24pm    
Thank you for pointing it out; somehow I missed this line... Hm...
--SA
I modified your code slightly so that it could be compiled using all .NET versions.

With .NET2 and 3.5 there was no difference in execution time between the standard and dynamic methods, both taking about 1400 - 1500ms in release mode.

However .NET 4 (v4.0.30319) was a different story altogether. The results were similar to yours with 1400 for the standard and 19700ms for the dynamic method.

My modified method, with the speedup fix commented out, is:
C#
public static Action<T1, T2> CreateCopier<T1, T2>() {
  // SLOW
  DynamicMethod meth = new DynamicMethod(
    "copy",
    null,
    new Type[] { typeof(T1), typeof(T2) },
    true);
  // FAST
  //DynamicMethod meth = new DynamicMethod(
  //  "copy",
  //  null,
  //  new Type[] { typeof(T1), typeof(T2) },
  //  typeof(Program),                  // associate with a type
  //  true);
  ILGenerator il = meth.GetILGenerator();
  int cpt = 0;

  foreach (PropertyInfo mi1 in typeof(T1).GetProperties(BindingFlags.Public | BindingFlags.Instance)) {
    PropertyInfo mi2 = typeof(T2).GetProperty(mi1.Name, BindingFlags.Public | BindingFlags.Instance);
    if (mi1 != null && mi2 != null) {
      cpt++;
      il.Emit(OpCodes.Nop);
      il.Emit(OpCodes.Ldarg_1);
      il.Emit(OpCodes.Ldarg_0);
      il.Emit(OpCodes.Callvirt, mi1.GetGetMethod());
      il.Emit(OpCodes.Callvirt, mi2.GetSetMethod());
    }
  }
  il.Emit(OpCodes.Ret);

  Delegate dlg = meth.CreateDelegate(typeof(Action<T1, T2>));
  return (Action<T1, T2>)dlg;
}


The big slow down in execution time is caused when the dynamic method is associated with an anonymous assembly by using the DynamicMethod(string, Type, Type[], bool) constructor. I would guess that .NET 4 is doing more security checks than the previous versions, although I have no insight into, or explanation for, what is actually going on.

Associating the method with a Type by using the DynamicMethod(string, Type, Type[], Type, bool) constructor completely removes the speed penalty.

There are some notes on MSDN which may be relevant (if only I could understand them!)
http://msdn.microsoft.com/en-us/library/bb348332(v=vs.100).aspx[^]
http://msdn.microsoft.com/en-us/library/9syytdak.aspx[^]

Alan.
 
Share this answer
 
Comments
Pascal Ganaye 17-Nov-12 10:37am    
Exactly
Well worth a five and my gratitude.
Nissim Salomon 1-Sep-13 0:40am    
Hi

I'm facing some issues with Dynamic Code Generation can you please tell me how did you Dump the DynamicMethod IL code ? what kind of tools & commands did you use ?

another challange (if you will choose to accept it :-))
the following code generate a slower execution path method why is that ?

public static Action<tsourcetype, ttaregettype=""> CreateCopierUsingSyntexTrees<tsourcetype, ttaregettype="">()
{
List<expression> exprs = new List<expression>();

PropertyInfo[] sourceTypeProperties =
typeof (TSourceType).GetProperties(BindingFlags.Public | BindingFlags.Instance);

ParameterExpression sourceParam = Expression.Variable(typeof (TSourceType), "source");
ParameterExpression targetParam = Expression.Variable(typeof(TSourceType), "target");

foreach (PropertyInfo sourceProperty in sourceTypeProperties)
{
exprs.Add(Expression.Assign(Expression.Property(targetParam, sourceProperty.Name), Expression.Property(sourceParam, sourceProperty)));
}

BlockExpression body = Expression.Block(exprs);

return Expression.Lambda<action<tsourcetype, ttaregettype="">>(body, new[] {sourceParam, targetParam}).Compile();
}

This content, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)



CodeProject, 20 Bay Street, 11th Floor Toronto, Ontario, Canada M5J 2N8 +1 (416) 849-8900