为何表达式委托比IL代码执行更快?附测试代码与性能数据
Great question! Let's break down why your expression tree-compiled delegate is outperforming the handwritten IL DynamicMethod in this test.
1. Expression Tree Compiler Optimizations & Hosting Differences
First, looking at your Test1 code: you wrapped the Add expression in an Expression.Block, but the LambdaCompiler (the code you referenced) is smart enough to eliminate unnecessary block nodes during compilation. The resulting IL for the expression tree delegate is essentially identical to what you wrote by hand in Test2—so that's not the source of the performance gap.
The key difference lies in how the two delegates are hosted:
- The expression tree compiler emits the compiled method into a dynamic assembly as a regular static method. The JIT treats these methods exactly like any other static method in your codebase, applying full optimizations like inlining, register allocation, and other low-level tweaks.
DynamicMethods, by contrast, are created in a special memory region designed for lightweight, temporary methods. Even withRuntimeHelpers.PrepareDelegate, the JIT may impose additional restrictions or overhead on these methods—for example, skipping certain optimizations like inlining, or adding extra safety checks during invocation that accumulate over millions of calls (like your loop runningint.MaxValue/10times).
2. DynamicMethod Visibility Flags Impact
Your DynamicMethod is created with the skipVisibility: true parameter. While this allows the method to access private members if needed, it also signals to the runtime that this method has special visibility characteristics. The JIT may handle such methods differently, adding a tiny but measurable per-invocation overhead that adds up over your massive loop.
If you modify the DynamicMethod constructor to target a specific type instead of using skipVisibility: true, you might see much closer performance:
// Anchor the dynamic method to your containing class instead of using skipVisibility var dm = new DynamicMethod("Add", typeof(int), new[] {typeof(int), typeof(int)}, typeof(YourContainingClass));
This makes the dynamic method behave more like the expression tree's compiled method in the eyes of the JIT.
3. Warm-Up & Upfront Compilation Work
While you did call f(1,2) before starting the stopwatch in both tests, the expression tree's Compile() method does more upfront work to prepare the method for JIT compilation. DynamicMethods, on the other hand, may defer some of this preparation until the first invocation—even with PrepareDelegate, there might be residual per-invocation overhead that doesn't get fully eliminated.
Quick Validation Test
To confirm the Expression.Block isn't a factor, try modifying Test1 to remove the unnecessary wrapper:
var expression = Expression.Lambda<Func<int,int,int>>(Expression.Add(a,b),a,b);
You'll likely see almost identical performance to your original Test1 result, since the compiler optimizes away the empty block automatically.
Summary
The performance gap boils down to runtime/JIT treatment of dynamically generated methods:
- Expression tree-compiled methods are regular static methods in dynamic assemblies, eligible for all standard JIT optimizations.
DynamicMethods have inherent overhead due to their special memory hosting and visibility settings, which the JIT doesn't optimize away as aggressively.
内容的提问来源于stack exchange,提问作者user3815881

