Optimizing Func.Invoke() generated from expression tree
Asked Answered
C

2

8

I am working on an automation for instantiating classes dynamically.

I decided to write an expression tree that would generate a Func, that could instantiate my class for me. However, I am noticing 3x slower performance of my Func as opposed to simply using new.

From what I know about expression trees and invoking functions, the performance difference should be almost non-existant (maybe 20-30%, but nowhere near 3 times slower)

First off, here is the expression that I am building

public Expression<Func<A1, T>> BuildLambda<T, A1>(string param1Name)
    {
        var createdType = typeof(T);

        var param = Expression.Parameter(typeof(A1), param1Name);
        var ctor = Expression.New(createdType);
        var prop = createdType.GetProperty(param1Name);

        var displayValueAssignment = Expression.Bind(prop, param);
        var memberInit = Expression.MemberInit(ctor, displayValueAssignment);

        return
            Expression.Lambda<Func<A1, T>>(memberInit, param);
    }

I then proceed to compile it like so (I do this only once)

var c1 = mapper.BuildLambda<Class1, int>("Id").Compile();

And then I invoke my Func like so

var result = c1.Invoke(5);

When I put this last part in a loop and compare it to something like

var result = new Class1() { Id = 5 };

I did a couple of tests, comparing the performance in both, and this is what I ended up with:

100,000    Iterations - new: 0ms.   | Func 2ms.
600,000    Iterations - new: 5ms.   | Func 14ms.
3,100,000  Iterations - new: 24ms.  | Func 74ms.
15,600,000 Iterations - new: 118ms. | Func 378ms.
78,100,000 Iterations - new: 597ms. | Func 1767ms.

As you can see my Func.Invoke() is roughly 2.5 - 3 times slower than instantiating using new. Does anyone have any tips on how I might improve this? (I don't mind using pure reflection as I manage to get better performance)

*For anyone who wants to test this here is a pastebin of my setup: https://pastebin.com/yvMLqZ2t

Constanceconstancia answered 4/1, 2018 at 16:30 Comment(15)
Could you provide a complete example of the benchmarking rather than just the snippets? That'll make it easier for us to experiment (and potentially spot any benchmarking issues).Specie
Yes, I will edit right nowConstanceconstancia
@JonSkeet Added the link to pastebin - pastebin.com/yvMLqZ2tConstanceconstancia
Any reason not to include that directly in the question? After removing the unused ExpressionParam class and the namespace declaration, it's only about 60 lines...Specie
Interestingly, trying the same thing with a delegate created from a lambda expression, e.g. j => new Class1 { Id = j }, gets somewhere between the two.Specie
What is also interesting is that this is an issue only on full .net framework. .Net Core behaves as expected.Wally
The manually-created expression tree performance is pretty much identical to the performance of an expression tree created by the compiler from a lambda expression, too.Specie
Related posts - Performance of compiled-to-delegate Expression, Compiled C# Lambda Expressions Performance, etc.Lackadaisical
Check out the answer in the first question recommended by @IvanStoev. If you add [assembly: AllowPartiallyTrustedCallers] to your code, the results of the two variants are pretty much the same (at most 1.5 times slower expression calls on my machine). Also, there is a link in that answer to another question with the same answer. I'm not sure why, though, so if someone can go deeper than 'this seems to be a bit of a bug', I'd be grateful too...Gunning
If you look into what are custom attributes for generated assembly in full framework, there is SecurityRulesAttribute with SecurityRuleSet.Level1. .Net Core generated assembly does not have this attribute, and by reading on this attribute and based on answer to first question recommended by @IvanStoev, this might be the culprit. Assemblies without this attribute specifically applied get SecurityRuleSet.Level2 value on framework 4.0 and higher.Wally
Thanks for all of the responses. The articles and insights you guys provided helped a lot, I don't have enought time to check all of them in practice atm, but I will report as soon as I have some results. Also, no particular reason for not adding the whole code in the question...didn't want to clutter it with code I guessConstanceconstancia
The assembly attributes did got the performance difference to only 20-25% slower execution (in Release). I am guessing the fact that without them .NET Framework puts the new methods in a different assembly, that requires a handshake process to access, results in the larger overhead. Correct me if I am wrong :) Anyways, thank you for all the info and links once again, this does seem to resolve my issueConstanceconstancia
Then maybe someone who suggested attribute solution will post\update answer, so author can accept it? as having standalone accepted answer is better than having it in commentsTurfy
Yeah, I was thinking the same thingConstanceconstancia
In case you were thinking about me, I'm not entirely comfortable posting the solution as an answer, since I have no other definitive explanation other than 'this other post said so and it works'. Maybe the question could be closed as a duplicate based on @IvanStoev 's comment.Gunning
G
2

After reading through all the posts in the comments, I came up with this idea: when you create a DynamicMethod instead of an expression-tree and you assign it logically to the module of the current executing code, you should not get this overhead.

I think (or at least hope) that you were looking for improvement options on the general idea, not specifically the expression-tree based version, so I'm posting this as an improvement option :)

So I tried this piece of code:

 public static Func<A1, T> BuildLambda<A1, T>(string propertyName)
 {
   // This is where the magic happens with the last parameter!!
   DynamicMethod dm = new DynamicMethod("Create", typeof(T), new Type[] { typeof(A1) }, typeof(Program).Module);

   // Everything else is just generating IL-code at runtime to create the class and set the property
   var setter = typeof(T).GetProperty(propertyName).SetMethod;
   var generator = dm.GetILGenerator();
   var local = generator.DeclareLocal(typeof(T));
   generator.Emit(OpCodes.Newobj, typeof(Class1).GetConstructor(Type.EmptyTypes));
   generator.Emit(OpCodes.Stloc, local);
   generator.Emit(OpCodes.Ldloc, local);
   generator.Emit(OpCodes.Ldarg_0);
   generator.Emit(OpCodes.Call, setter);
   generator.Emit(OpCodes.Ldloc, local);
   generator.Emit(OpCodes.Ret);
   return (Func<A1, T>)dm.CreateDelegate(typeof(Func<A1, T>));
}

And on my machine this produced delegates that are executed max 1.8 times slower than the hand-written code, without specifying the attribute. Not 1.5, but at least I don't have to include an assembly-wide attribute to my code that I don't fully understand:)

Note that if you omit the last parameter of the DynamicMethod constructor, you still get the even slower results for the generated code.

EDIT

I stumbled upon this blog post, which poses the same question and gives the same solution:

https://blogs.msdn.microsoft.com/seteplia/2017/02/01/dissecting-the-new-constraint-in-c-a-perfect-example-of-a-leaky-abstraction/

Gunning answered 4/1, 2018 at 18:7 Comment(2)
Tried it and I did manage to get 1.3 - 1.6 times slower execution. I guess the "magic" in typeof(Program).Module, basically revolves around that fact, that adding the method as part of the current assembly, means there is no other assembly do establish a handshake process with. (Which is basically what the assembly attributes help get around to). Thank you for the suggestion. I might use it if i do not find anything more benefitial :)Constanceconstancia
Something like that. Note that assemblies are immutable; once it's done it's done, you cannot add anything to them physically. Modules are smaller code units that make up assemblies, you just can't see them if you write your asm in C#. This means that you cannot add anything to a module either after compilation. The docs suggest that the last parameter creates a logical association. Since it's not physical, it's not added to the asm itself, just somehow linked to it from runtime (e.g. security) aspects, I guess. Maybe :)Gunning
H
0

Let me try something different. The thing you might do is currying:

Func<TArg, TRes> BuildFuncFor<TClass, TArg, TRes>(Func<TClass> typeCreator, Action<TArg, TClass> argumentAssigner) {
        return arg => {
             var type = typeCreator();
             argumentAssigner(arg, type);
             return type;
    }
}

Then, the same currying approach might be applied to supply both default/dynamic implementation of both func's. A typical typeCreator would be something alike Activator.Create(...). Depending upon your logic, there might be more functions required; for example: Func<object[]> constructorArgumentsSupplier. Same applies to the assigning a given value to a given property: good-old reflection: exactly the way WPF does.

But most of them 1) might be created only once for a certain type and cached for further usage; 2) precompiled rather than rely on expressions, which is kind of pain.

Hexane answered 4/1, 2018 at 19:19 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.