DynamicMethod is much slower than compiled IL function
Asked Answered
S

2

10

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

Note that - as I run the dynamic method before starting the stopwatch - the duration do not include the compilation phase. I run that in Debug and Release mode, in x86 and x64 mode, and from VS and from the command line with roughly the same result (dynamic method is 400% slower).

        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.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.

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

I applogize if you read this twice. I posted this originally in: http://www.codeproject.com/Answers/494714/Can-27tplusfigureplusoutpluswhyplusthisplusDynamic but did not get all the answers I hoped.

edited 17 nov 2012 15:11:

removed the nop
removed the extra ="" which came from I don't where.
Seger answered 17/11, 2012 at 14:38 Comment(5)
You shouldn't be emitting "nop", btw. The fact that you have "nop" suggests you are in debug mode. Can you run it in release mode, outside of the IDE? And without the "nop"? What timings do you get then?Eberto
What is Action<T1, T2=""> supposed to mean? It's certainly not valid C#.Warrior
@JonSkeet If I remove all of the =""s from the code, it does compile and it behaves as Pascal described.Warrior
@svick: Fair enough... but obviously we shouldn't have to do that... (some of the whitespace choices are distinctly readability-hostile too)Ty
And now the first 14 lines have been removed...Ty
S
11

This problem was introduced by changes made in .NET Framework 4.0. I found a solution posted by user "Alan-N" on CodeProject.

The big slowdown in execution time is caused when the DynamicMethod gets associated with a "system-provided, fully trusted, security-transparent assembly," which happens if you use the DynamicMethod(string, Type, Type[], bool) constructor. It appears 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 DynamicMethod with a Type (by using the DynamicMethod(string, Type, Type[], Type, bool) constructor instead; notice the additional Type-valued parameter, 'owner') completely removes the speed penalty.

There are some notes on MSDN which may be relevant (if only I could understand them!):

Seger answered 17/11, 2012 at 15:34 Comment(1)
This makes sense, there's an extra level of indirection involved when invoking 'secure' delegates, rather than regular ones, see mattwarren.org/2017/01/25/How-do-.NET-delegates-work/… for some infoDachau
C
14

This is a bit late, but if you set a few security attributes in .NET 4 on all your assemblies and you use built-in delegate types—or delegates with the same security attributes—you will see quite a performance gain.

Here are the attributes you will want:

[assembly: AllowPartiallyTrustedCallers]
[assembly: SecurityTransparent]
[assembly: SecurityRules(SecurityRuleSet.Level2,SkipVerificationInFullTrust=true)]

This actually seems to be a bit of a bug. But because you are saying that your code will not raise security permissions, you will not block partially-trusted callers, so if you use skipVisibility=true in full trust, invoking a Func<int,int> delegate should basically avoid almost all of the permission checks.

One more thing, since these are delegates you will get the best performance if you treat them like instance methods, even though they are not. That is to say always use one of the two Delegate.CreateDelegate methods that accepts a firstArgument parameter and add an initial object reference to your delegate.

Consider constructing the DynamicMethod with skipVisibility=true, but without assigning an owner. Assigning an owner allows you to run unverifiable code. You can do some really screwed up things with this, so I would avoid it unless you know what you are doing.

Calci answered 12/1, 2013 at 19:27 Comment(5)
The assembly attributes you posted are a good solution when the owner parameter cannot be specified. AFAIK this is the case when generating a method using C# Expression Trees.Nemertean
This helped me a lot recently to improve performance, my only concern is does adding these attributes have a side-effect I should be worried about?Polley
Potentially, using the SecurityTransparent can be annoying if you are referencing dlls that do not mark their code appropriately.Calci
@MichaelB Regarding third-party libraries with poorly-attributed security, is there a way to re-specify or override such deficient—or fully insufficient—specifications by using an assembly binding redirect in the application manifest of the hosting app (or some other means)?Postglacial
I sadly cannot say. I would wager you may want to assign an owner, but be careful about the code you generate. Provided the owning Type is from a secure location it should run faster than non-secure code. .Net core does not have this issue.Calci
S
11

This problem was introduced by changes made in .NET Framework 4.0. I found a solution posted by user "Alan-N" on CodeProject.

The big slowdown in execution time is caused when the DynamicMethod gets associated with a "system-provided, fully trusted, security-transparent assembly," which happens if you use the DynamicMethod(string, Type, Type[], bool) constructor. It appears 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 DynamicMethod with a Type (by using the DynamicMethod(string, Type, Type[], Type, bool) constructor instead; notice the additional Type-valued parameter, 'owner') completely removes the speed penalty.

There are some notes on MSDN which may be relevant (if only I could understand them!):

Seger answered 17/11, 2012 at 15:34 Comment(1)
This makes sense, there's an extra level of indirection involved when invoking 'secure' delegates, rather than regular ones, see mattwarren.org/2017/01/25/How-do-.NET-delegates-work/… for some infoDachau

© 2022 - 2024 — McMap. All rights reserved.