缓存编译的表达式树

时间:2013-10-30 09:13:09

标签: c# expression-trees

如何有效地缓存从表达式树编译的方法?

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将失败。

6 个答案:

答案 0 :(得分:15)

在集中式缓存中缓存表达式树的问题是:

  1. 您需要全面的相等比较和散列算法。
  2. 您需要自己实现这些算法,因为标准表达式类型不能提供开箱即用的功能。
  3. 全面的相等比较执行起来会很昂贵,但是使用便宜的哈希函数可以稍微降低成本。此外,由于表达式树是不可变的,因此您可以在第一次计算它之后缓存哈希码。这可能会减少一些查找时间,但每次使用新创建的表达式作为键检查缓存(我想,大部分时间都是这样),您至少需要对新表达式进行哈希处理。

    选项1:本地/静态缓存

    理想的解决方案可以避免所有这些开销。如果它是可行的(即,如果这些表达式不是动态组合的),那么最好的办法是简单地将表达式树缓存在其声明站点附近。您应该能够将大部分(如果不是全部)这些存储在静态字段中:

    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);
    }
    

    缺点是你最终可能会遇到各种类型的重复表达,但如果你没有生成大量的表达式,这可能不是问题。

    选项2:集中缓存

    如果这不是您的选择,则必须实现集中式缓存以及执行此操作所需的散列和相等算法。散列算法将帮助您缩小候选集的范围,因此非常重要的是合理地,即在实践中产生非常少的冲突。但是,鉴于表达式树的复杂性,您希望降低成本。因此,我建议你不要追求一个完美的散列函数,而是一个相当便宜但有效的函数。也许是这样的:

    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);
            }
        }
    }
    

    它并不完美,但它应该会给你带来不错的效果:

    1. 向下钻取并包含树中的每个表达。
    2. 至少,每个子表达式的NodeType都包含在哈希中。一个明显(但可能代价很高)的改进是包括Type;如果你发现碰撞太多,请试试。
    3. 包含表达式中引用的成员和类型。
    4. 包含树中出现的常量值。
    5. 它不需要运行堆分配,代价是不可重入(因为您只分析顶级表达式,这很好)。您可以在多个线程上同时运行它。
    6. 由于您实际上没有为任何表达式类型覆盖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很可能返回表达式的内存地址。

要解决此问题,您可以引入更“智能”的哈希计算。让我们考虑具有相同主体的相等表达式。这是非常滑的道路,但如果你愿意承担责任确保:

  1. 没有两个不同的表达式具有相同的哈希码
  2. 具有相同主体的两个表达式具有相同的哈希码
  3. 你可以达到你想要的效果。

    这是一个简单的proof of concept code我已经在pastebin为你聚集了。这不是工业实力(评论中的一些提示可以改进它),但它清楚地证明了方法的可行性。

    在进一步阐述之前需要考虑的几件事:不正确的哈希函数可能会导致相当棘手的错误。所以,三思而后行,写下很多单元测试和猎物:)

答案 3 :(得分:1)

您描述的问题非常严重,因为评估语义相等的两个表达式至少与编译表达式一样昂贵。为了说明这一点,here是表达式相等的实现的链接。这种实现并不完美,例如:

MethodA() { MethodB(); }
MethodB() { ... }

在上面的示例中,MethodAMethodB在他们做同样事情的意义上是等效的,您很可能希望将它们视为等效。例如,在启用编译器优化的情况下在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);
    }