如何有效地缓存从表达式树编译的方法?
public void SomeToStringCalls()
{
ToString(i => (i + 1).ToString(), 1);
ToString(i => (i + 1).ToString(), 2);
ToString(i => (i + 2).ToString(), 3);
ToString(i => (i + 2).ToString(), 4);
}
private string ToString<T>(Expression<Func<T, string>> expression, T input)
{
var method = expression.Compile();
return method.Invoke(input);
}
上面,每个调用都会重新编译每个表达式,即使它们是相同的。我不能从表达式中Dictionary<Expression<Func<T, string>>, Func<T, string>>()
缓存已编译的方法,因为equals
将失败。
答案 0 :(得分:15)
在集中式缓存中缓存表达式树的问题是:
全面的相等比较执行起来会很昂贵,但是使用便宜的哈希函数可以稍微降低成本。此外,由于表达式树是不可变的,因此您可以在第一次计算它之后缓存哈希码。这可能会减少一些查找时间,但每次使用新创建的表达式作为键检查缓存(我想,大部分时间都是这样),您至少需要对新表达式进行哈希处理。
理想的解决方案可以避免所有这些开销。如果它是可行的(即,如果这些表达式不是动态组合的),那么最好的办法是简单地将表达式树缓存在其声明站点附近。您应该能够将大部分(如果不是全部)这些存储在静态字段中:
private static readonly Expression<Func<int, string>> _addOne =
i => (i + 1).ToString();
private static readonly Expression<Func<int, string>> _addTwo =
i => (i + 2).ToString();
public void SomeToStringCalls()
{
ToString(_addOne, 1);
ToString(_addOne, 2);
ToString(_addTwo, 3);
ToString(_addTwo, 4);
}
缺点是你最终可能会遇到各种类型的重复表达,但如果你没有生成大量的表达式,这可能不是问题。
如果这不是您的选择,则必须实现集中式缓存以及执行此操作所需的散列和相等算法。散列算法将帮助您缩小候选集的范围,因此非常重要的是合理地,即在实践中产生非常少的冲突。但是,鉴于表达式树的复杂性,您希望降低成本。因此,我建议你不要追求一个完美的散列函数,而是一个相当便宜但有效的函数。也许是这样的:
internal static class ExpressionHasher
{
private const int NullHashCode = 0x61E04917;
[ThreadStatic]
private static HashVisitor _visitor;
private static HashVisitor Visitor
{
get
{
if (_visitor == null)
_visitor = new HashVisitor();
return _visitor;
}
}
public static int GetHashCode(Expression e)
{
if (e == null)
return NullHashCode;
var visitor = Visitor;
visitor.Reset();
visitor.Visit(e);
return visitor.Hash;
}
private sealed class HashVisitor : ExpressionVisitor
{
private int _hash;
internal int Hash
{
get { return _hash; }
}
internal void Reset()
{
_hash = 0;
}
private void UpdateHash(int value)
{
_hash = (_hash * 397) ^ value;
}
private void UpdateHash(object component)
{
int componentHash;
if (component == null)
{
componentHash = NullHashCode;
}
else
{
var member = component as MemberInfo;
if (member != null)
{
componentHash = member.Name.GetHashCode();
var declaringType = member.DeclaringType;
if (declaringType != null && declaringType.AssemblyQualifiedName != null)
componentHash = (componentHash * 397) ^ declaringType.AssemblyQualifiedName.GetHashCode();
}
else
{
componentHash = component.GetHashCode();
}
}
_hash = (_hash * 397) ^ componentHash;
}
public override Expression Visit(Expression node)
{
UpdateHash((int)node.NodeType);
return base.Visit(node);
}
protected override Expression VisitConstant(ConstantExpression node)
{
UpdateHash(node.Value);
return base.VisitConstant(node);
}
protected override Expression VisitMember(MemberExpression node)
{
UpdateHash(node.Member);
return base.VisitMember(node);
}
protected override MemberAssignment VisitMemberAssignment(MemberAssignment node)
{
UpdateHash(node.Member);
return base.VisitMemberAssignment(node);
}
protected override MemberBinding VisitMemberBinding(MemberBinding node)
{
UpdateHash((int)node.BindingType);
UpdateHash(node.Member);
return base.VisitMemberBinding(node);
}
protected override MemberListBinding VisitMemberListBinding(MemberListBinding node)
{
UpdateHash((int)node.BindingType);
UpdateHash(node.Member);
return base.VisitMemberListBinding(node);
}
protected override MemberMemberBinding VisitMemberMemberBinding(MemberMemberBinding node)
{
UpdateHash((int)node.BindingType);
UpdateHash(node.Member);
return base.VisitMemberMemberBinding(node);
}
protected override Expression VisitMethodCall(MethodCallExpression node)
{
UpdateHash(node.Method);
return base.VisitMethodCall(node);
}
protected override Expression VisitNew(NewExpression node)
{
UpdateHash(node.Constructor);
return base.VisitNew(node);
}
protected override Expression VisitNewArray(NewArrayExpression node)
{
UpdateHash(node.Type);
return base.VisitNewArray(node);
}
protected override Expression VisitParameter(ParameterExpression node)
{
UpdateHash(node.Type);
return base.VisitParameter(node);
}
protected override Expression VisitTypeBinary(TypeBinaryExpression node)
{
UpdateHash(node.Type);
return base.VisitTypeBinary(node);
}
}
}
它并不完美,但它应该会给你带来不错的效果:
NodeType
都包含在哈希中。一个明显(但可能代价很高)的改进是包括Type
;如果你发现碰撞太多,请试试。由于您实际上没有为任何表达式类型覆盖GetHashCode()
,因此散列函数的缺陷不会泄漏并影响外部代码。这为我们提供了一定程度的自由来弯曲哈希函数的规则。
您的平等比较需要更加全面。虽然散列函数实际上是用于最小化候选集的“估计”,但是相等比较执行实际匹配,并且它需要是完美的。你当然可以使用我提出的散列解决方案作为如何来解决问题的模板,但请记住,你必须对所有表达式的属性进行详尽的比较。要记住的一件事是,您可能不希望比较树中ParameterExpression
节点的名称,但是您需要在两棵树中维护参数/变量的映射。比较以确保它们在父表达式树的上下文中表示“相同”值。
除了忽略参数/变量名称之外,不要试图解决“语义等价”,即产生相同结果和副作用但结构上不相同的表达式。它不能有效地完成,并且不值得尝试。
最后,您可以通过实现两级查找来加快速度:首先,根据参数类型选择正确的缓存,然后在该缓存中查找匹配项。如果能保证每个lambda表达式只有一个参数,那么这种方法最有效。随着更多的争论,好处会降低。
答案 1 :(得分:5)
我很久以前发现了this article,这是暴露专业人士和表达式缓存的缺点(使用常量提取...允许您将.Where(t=>t.prop==3)
和.Where(t=>t.prop==5)
编译到同一个委托中。
答案 2 :(得分:2)
您无法使用Dictionary<Expression<Func<T, string>>, Func<T, string>>
的原因是Expression<T>
GetHashCode
不够智能,无法检测“相等”的表达式。我不确定,但Expression<T>.GetHashCode
很可能返回表达式的内存地址。
要解决此问题,您可以引入更“智能”的哈希计算。让我们考虑具有相同主体的相等表达式。这是非常滑的道路,但如果你愿意承担责任确保:
你可以达到你想要的效果。
这是一个简单的proof of concept code我已经在pastebin为你聚集了。这不是工业实力(评论中的一些提示可以改进它),但它清楚地证明了方法的可行性。
在进一步阐述之前需要考虑的几件事:不正确的哈希函数可能会导致相当棘手的错误。所以,三思而后行,写下很多单元测试和猎物:)
答案 3 :(得分:1)
您描述的问题非常严重,因为评估语义相等的两个表达式至少与编译表达式一样昂贵。为了说明这一点,here是表达式相等的实现的链接。这种实现并不完美,例如:
MethodA() { MethodB(); }
MethodB() { ... }
在上面的示例中,MethodA
和MethodB
在他们做同样事情的意义上是等效的,您很可能希望将它们视为等效。例如,在启用编译器优化的情况下在C#中构建此代码将使用MethodB
调用替换MethodA
调用。比较代码时存在无数问题,这是正在进行的研究的主题。
如果您发现编译表达式是应用程序中的瓶颈,您应该考虑一种设计,其中表达式由某种键引用,用于标识表达式。 当您确定了相等时,您可以编译它。
要评论J0HN的答案,它会比较正文的哈希码和参数,这绝不是一个可靠的解决方案,因为它不会对表达式树进行深入评估。
另外,请查看评论中发布的this question。
答案 4 :(得分:0)
如果您的目标是从表达式编译+调用“提取值”,那么您可能会采用另一种方式。
我尝试从表达式树中提取值而不通过反射进行编译。
我的解决方案并不完全支持所有表达式,起初它是为没有lambda和算术的缓存方法调用而编写的,但是通过一些改进它可以提供帮助。
这是:
private static object ExtractValue(Expression expression, object[] input, ReadOnlyCollection<ParameterExpression> parameters)
{
if (expression == null)
{
return null;
}
var ce = expression as ConstantExpression;
if (ce != null)
{
return ce.Value;
}
var pe = expression as ParameterExpression;
if (pe != null)
{
return input[parameters.IndexOf(pe)];
}
var ma = expression as MemberExpression;
if (ma != null)
{
var se = ma.Expression;
object val = null;
if (se != null)
{
val = ExtractValue(se, input, parameters);
}
var fi = ma.Member as FieldInfo;
if (fi != null)
{
return fi.GetValue(val);
}
else
{
var pi = ma.Member as PropertyInfo;
if (pi != null)
{
return pi.GetValue(val);
}
}
}
var mce = expression as MethodCallExpression;
if (mce != null)
{
return mce.Method.Invoke(ExtractValue(mce.Object, input, parameters), mce.Arguments.Select(a => ExtractValue(a, input, parameters)).ToArray());
}
var sbe = expression as BinaryExpression;
if (sbe != null)
{
var left = ExtractValue(sbe.Left, input, parameters);
var right = ExtractValue(sbe.Right, input, parameters);
// TODO: check for other types and operands
if (sbe.NodeType == ExpressionType.Add)
{
if (left is int && right is int)
{
return (int) left + (int) right;
}
}
throw new NotImplementedException();
}
var le = expression as LambdaExpression;
if (le != null)
{
return ExtractValue(le.Body, input, le.Parameters);
}
// TODO: Check for other expression types
var dynamicInvoke = Expression.Lambda(expression).Compile().DynamicInvoke();
return dynamicInvoke;
}
使用方法:
private static string ToString<T>(Expression<Func<T, string>> expression, T input)
{
var sw = Stopwatch.StartNew();
var method = expression.Compile();
var invoke = method.Invoke(input);
sw.Stop();
Console.WriteLine("Compile + Invoke: {0}, {1} ms", invoke, sw.Elapsed.TotalMilliseconds);
sw.Restart();
var r2 = ExtractValue(expression, new object[] {input}, null);
sw.Stop();
Console.WriteLine("ExtractValue: {0}, {1} ms", r2, sw.Elapsed.TotalMilliseconds);
return invoke;
}
我认为,通过一些改进和其他表达类型,这个解决方案可以更快地替代Compile()。DynamicInvoke()
答案 5 :(得分:-1)
简单地称呼我,但在我测试的一个简单场景中,这似乎快了4倍:
public static Dictionary<string, object> cache
= new Dictionary<string, object>();
public static string ToString<T>(
Expression<Func<T, string>> expression,
T input)
{
string key = typeof(T).FullName + ":" + expression.ToString();
object o; cache.TryGetValue(key, out o);
Func<T, string> method = (Func<T, string>)o;
if (method == null)
{
method = expression.Compile();
cache[key] = method;
}
return method.Invoke(input);
}