c# Expression.Compile() 的性能开销和并发缓存

来源:这里教程网 时间:2026-02-21 17:39:18 作者:

Expression.Compile() 为什么不能每次都调用

每次调用

Expression.Compile()
都会触发 IL 生成、JIT 编译和委托创建,底层涉及
DynamicMethod
AssemblyBuilder
(.NET Core/5+ 默认走
DynamicMethod
),实际开销远高于普通方法调用。在高并发场景下,反复编译同一表达式会导致 CPU 突增、GC 压力上升,甚至触发 JIT 线程争用。

典型错误模式是:在循环里或高频请求中对相同结构的

Expression<func bool>></func>
反复
Compile()
,比如动态构建查询条件时没做缓存。

编译一次平均耗时约 0.1–1ms(取决于表达式复杂度),但并发 100 线程同时编译同一表达式,可能产生上百个等效但互不共享的委托实例 生成的委托对象不可回收——它们被 JIT 和运行时内部强引用,长期驻留内存 .NET 6+ 对简单表达式有轻微优化(如常量折叠),但不改变编译本质开销

手动缓存 Expression.Compile() 结果的正确姿势

缓存核心是「键唯一性 + 线程安全」。不能只用

Expression.ToString()
当 key——它不稳定(节点顺序、调试信息可能变),也不反映语义等价性。推荐用表达式结构哈希或标准化后比较。

最简可行方案是用

ConcurrentDictionary<string delegate></string>
,key 由表达式类型 + 关键参数组合生成(例如字段名、操作符、字面值):

private static readonly ConcurrentDictionary<string, Func<Person, bool>> _cache 
    = new();
<p>public static Func<Person, bool> GetFilter(string propertyName, object value)
{
var key = $"Person_{propertyName}<em>Equals</em>{value}";
return <em>cache.GetOrAdd(key, </em> => 
{
var param = Expression.Parameter(typeof(Person));
var body = Expression.Equal(
Expression.Property(param, propertyName),
Expression.Constant(value)
);
return Expression.Lambda<Func<Person, bool>>(body, param).Compile();
});
}
避免把整个
Expression
对象当 key(它没重写
GetHashCode
,默认是引用哈希)
不要在 lambda 捕获外部变量(如
value
是局部变量),否则闭包会阻止委托被缓存复用
若表达式含
Expression.Constant(new object())
,该对象每次新建,导致 key 不一致——应提前提取为静态只读字段

Expression.Compile() 在 ASP.NET Core 中的常见误用

在控制器或中间件里动态构建并编译表达式(如基于查询字符串生成过滤器),极易成为性能瓶颈点。框架本身不帮你缓存,全靠自己设计生命周期。

Scoped 服务里缓存需注意:Scoped 实例随请求创建,缓存无法跨请求共享——应提升到 Singleton 服务中 使用
Microsoft.Extensions.Caching.Memory.IMemoryCache
时,key 必须可序列化且稳定;建议用
MemoryCacheEntryOptions.SlidingExpiration
防止无限增长
EF Core 的
Where(expression)
内部已缓存编译结果(针对相同表达式树结构),但仅限于 EF 自己解析的子集;手写复杂表达式仍需自行管理

替代方案:Expression.Compile() 的轻量级选项

如果只是需要“类似委托”的调用能力,且表达式结构固定,可考虑绕过编译:

Expression.Evaluate()
(需引入
System.Linq.Expressions.Extensions
第三方包)——解释执行,无编译开销,适合低频、调试场景
预定义一组常用表达式模板(如
static readonly Expression<func bool>> EqualTo = ...</func>
),运行时替换参数节点再编译,减少重复生成
.NET 7+ 支持
Expression.CompileFast()
(非官方 API,来自
FastExpressionCompiler
库),比原生快 2–5 倍,且支持更广的表达式语法

真正复杂的动态逻辑(如用户自定义规则引擎),建议直接上 Roslyn 编译 C# 字符串为程序集,虽然启动慢,但执行期零开销,且可卸载。

相关推荐