这个编译好的表达式树...
var param = Expression.Parameter(typeof(int));
var innerParam = Expression.Parameter(typeof(Action<int>));
var inner = Expression.Lambda(innerParam.Type, Expression.Block(), "test", new[] { param });
var outer = Expression.Lambda<Action<int>>(Expression.Block(new[] { innerParam }, Expression.Assign(innerParam, inner), Expression.Invoke(innerParam, param)), param).Compile();
似乎会导致在每次调用
outer
时创建一个新委托 - 如果例如在 Delegate.CreateDelegateNoSecurityCheck
中设置断点,则可观察到。
相比之下,该函数的基于非表达式的等价物
Action<int> inner = _ => { };
Action<int> outer = x =>
{
Action<int> innerParam = inner;
innerParam(x);
};
似乎没有这样做;重复调用
outer
不需要任何新的委托分配。
我很难理解为什么。这是故意的吗?是否有任何巧妙的技巧可以缓存基于表达式的版本的委托?
对于上下文:这是在使用外部反序列化库时出现的,该库似乎通过在我们的进程中创建委托来分配不合理的内存量。实际上,它做了非常相似的事情 - 它通过表达式树创建反序列化器,并将委托分配给局部变量以支持递归和循环反序列化。
此处的选项之一是编译内部 lambda 并调用它:
var compile = inner.Compile();
var method = compile.GetType().GetMethod(nameof(Action<int>.Invoke));
var outerExpression = Expression.Lambda<Action<int>>(Expression.Call(Expression.Constant(compile), method, param), param);
var outer = outerExpression.Compile();
var totalAllocatedBytes = GC.GetTotalAllocatedBytes();
for (int i = 0; i < 1000_000; i++)
{
outer(1);
}
Console.WriteLine(GC.GetTotalAllocatedBytes() - totalAllocatedBytes);