expression.compile() 首次调用慢因触发完整动态代码生成与jit编译流程,含il生成、元数据构造和机器码编译;后续调用缓存委托则近乎零开销。

Expression.Compile() 为什么首次调用特别慢
因为 Expression.Compile() 实际触发了 .NET 的动态代码生成与 JIT 编译全流程:它把表达式树翻译成 IL,写入内存模块,再交由 JIT 编译为本地机器码。这个过程涉及语法验证、类型检查、IL 验证、元数据生成和 JIT 编译 —— 和编译一个小型 .dll 差不多重。
常见现象:Compile() 在首次执行时耗时几十毫秒甚至上百毫秒(尤其含嵌套、泛型或复杂闭包时),但后续调用已编译委托几乎无开销。
- 表达式越深(如 10 层嵌套
Expression.Call),IL 生成阶段越慢 - 涉及泛型类型推导(如
Expression.Lambda<func int>></func>中T未固定)会额外触发反射绑定 - 捕获外部变量(闭包)会导致编译器生成匿名类,增加类型构造成本
缓存编译结果是最简单有效的优化
绝大多数场景下,相同结构的表达式树只需编译一次。直接缓存 Delegate 或强类型委托实例即可跳过重复编译。
注意别缓存表达式树本身(它不可重用且线程不安全),而是缓存编译后的委托:
private static readonly ConcurrentDictionary<string, Func<object, object>> _compiledCache
= new();
public static Func<object, object> GetAccessor(string propertyName)
{
return _compiledCache.GetOrAdd(propertyName, key =>
{
var param = Expression.Parameter(typeof(object));
var cast = Expression.Convert(param, typeof(MyClass));
var prop = Expression.Property(cast, key);
var convert = Expression.Convert(prop, typeof(object));
return Expression.Lambda<Func<object, object>>(convert, param).Compile();
});
}
- 用
ConcurrentDictionary而非Dictionary,避免多线程首次竞争编译 - 键要能唯一标识表达式逻辑(比如属性名 + 类型 FullName),避免不同语义却同名导致误复用
- 不要在循环内反复调用
Compile(),哪怕表达式树是“新 new 出来的”
用 Expression.CompileFast 替代原生 Compile(.NET Core 3.0+ 可省略)
.NET Core 3.0 起,Expression.Compile() 内部已大幅优化,性能接近 CompileFast;但在 .NET Framework 或需极致启动速度时,第三方库仍有价值。
CompileFast(来自 FastExpressionCompiler 包)绕过了部分反射验证和 IL 验证步骤,对复杂表达式提速明显(实测快 2–5 倍),且支持调试符号生成。
- 安装:
dotnet add package FastExpressionCompiler - 替换调用:
expr.CompileFast<func>>()</func>而非expr.Compile() - 注意:它不支持所有表达式节点(如
Expression.Block某些变体),失败会回退到原生Compile
哪些情况根本不该用 Expression.Compile()
不是所有“动态逻辑”都适合表达式树。当性能敏感、逻辑简单或仅需少量分支时,硬编码或 switch + Func 字段更轻量。
- 比如根据字符串字段名取值:用
PropertyInfo.GetValue()(慢但够用)或提前生成字典映射(Dictionary<string func obj>></string>)比每次建树+编译更快 - 枚举转换、状态机跳转等,用
static readonly Dictionary<t func>></t>预编译好委托,比运行时拼表达式更可控 - 涉及 I/O、锁、async/await 的逻辑,表达式树无法直接表示,强行封装反而增加抽象泄漏和调试难度
表达式树本质是“在运行时构造可编译代码”,它解决的是“结构不确定但模式固定”的问题。一旦模式也固定了,就该让位给更直接的实现。











