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:

  1. Argument Validation: The runtime verifies that the target instance and arguments match the method's parameter signature.
  2. Boxing and Unboxing: Value types must be boxed into object arrays and subsequently unboxed.
  3. Security Checks: Member accessibility and security boundaries are re-checked.
  4. 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 as Type or MemberInfo).
  • Prefer Strongly Typed Delegates: Use precise generic signatures (Func<TTarget, TProperty>) rather than generalized Func<object, object> variants to eliminate unboxing overhead.
  • Alternative Approaches: For extreme performance requirements without building expression trees manually, consider DynamicMethod with direct IL emission (ILGenerator), or compile-time source generators to avoid runtime generation entirely.