Expression.Compile for Fast Dynamic Code in C
In modern C# development, dynamically invoking methods or accessing
properties at runtime is a common requirement, yet relying on standard
reflection introduces severe performance penalties.
Expression.Compile resolves this issue by converting
runtime-defined expression trees into strongly typed delegates compiled
directly into Intermediate Language (IL). This article explains how
Expression.Compile works, why it outperforms pure
reflection, and how to use it to optimize dynamic execution paths in
high-throughput applications.
The Overhead of Pure Reflection
Traditional reflection in .NET relies on types such as
MethodInfo, PropertyInfo, and
FieldInfo. When invoking a member dynamically using
reflection:
MethodInfo method = typeof(Calculator).GetMethod("Add");
object result = method.Invoke(calculatorInstance, new object[] { 5, 10 });Every single invocation through MethodInfo.Invoke
triggers several costly operations:
- Argument Validation: The runtime verifies that the target instance and arguments match the method's parameter signature.
- Boxing and Unboxing: Value types must be boxed into
objectarrays and subsequently unboxed. - Security Checks: Member accessibility and security boundaries are re-checked.
- Call Indirection: The call cannot be optimized or inlined by the JIT compiler, leading to pointer indirection overhead on every iteration.
While acceptable for infrequent tasks, reflection becomes a severe bottleneck in scenarios like ORMs, dependency injection containers, and object mappers that execute millions of calls.
What Is Expression.Compile?
An expression tree (Expression<TDelegate>)
represents executable code as an in-memory Abstract Syntax Tree (AST).
Instead of executing instructions immediately, the tree models
parameters, operations, conditions, and method calls as structured data
objects.
The Compile method processes this AST using the CLR's
Lightweight Code Generation (LCG) infrastructure. Under the hood, it
creates a DynamicMethod, emits raw CIL (Common Intermediate
Language) opcodes directly representing the operations defined in the
tree, and bakes them into a callable native-speed delegate (such as
Func<T> or Action<T>).
using System;
using System.Linq.Expressions;
// 1. Define parameters: (Calculator instance, int a, int b)
var instanceParam = Expression.Parameter(typeof(Calculator), "calc");
var aParam = Expression.Parameter(typeof(int), "a");
var bParam = Expression.Parameter(typeof(int), "b");
// 2. Define the method call: calc.Add(a, b)
var methodCall = Expression.Call(instanceParam, typeof(Calculator).GetMethod("Add"), aParam, bParam);
// 3. Build and compile the expression tree into a typed delegate
var addFunc = Expression.Lambda<Func<Calculator, int, int, int>>(
methodCall, instanceParam, aParam, bParam
).Compile();
// 4. Fast execution
int result = addFunc(calculatorInstance, 5, 10);Why Expression.Compile Is Faster
The primary performance gain of Expression.Compile comes
from shifting the overhead from invocation time to compilation
time.
- No Boxing or Indirection: Because the dynamic code is compiled into a strongly typed delegate, primitive values are passed directly via CPU registers or the stack without memory allocations or type casting.
- JIT-Optimized Execution: Once compiled, the generated IL is treated by the runtime almost identically to standard compiled C# code. Calling the resulting delegate has the same execution speed as invoking a regular delegate or interface call.
- Cached Invocations: While calling
Compile()is computationally expensive, compilation only needs to happen once. By caching the compiled delegate in a concurrent dictionary or static field, subsequent invocations avoid all reflective checks and run at near-native speeds.
Performance Summary and Best Practices
| Mechanism | Setup Cost | Execution Cost | Allocations per Call |
|---|---|---|---|
Pure Reflection
(Invoke) |
Very Low | High | High (Argument arrays & boxing) |
Expression.Compile |
High | Extremely Low | None (for typed value types) |
| Direct C# Call | None | Extremely Low | None |
To maximize the benefits of Expression.Compile:
- Always Cache Delegates: Never call
.Compile()inside loops or hot paths; cache the generated delegate using cache keys (such asTypeorMemberInfo). - Prefer Strongly Typed Delegates: Use precise
generic signatures (
Func<TTarget, TProperty>) rather than generalizedFunc<object, object>variants to eliminate unboxing overhead. - Alternative Approaches: For extreme performance
requirements without building expression trees manually, consider
DynamicMethodwith direct IL emission (ILGenerator), or compile-time source generators to avoid runtime generation entirely.