【问题标题】:Fast way to get Expression method call target获取表达式方法调用目标的快速方法
【发布时间】:2019-01-09 14:10:20
【问题描述】:

给定以下代码行,

Expression<Action> expression = () => target.ToString();

有没有快速的方法来获取target对象?

以下代码有效

public object GetExpressionTarget<T>(Expression<T> expression)
{
    MethodCallExpression methodCall = (MethodCallExpression) expression.Body;
    LambdaExpression theTarget = Expression.Lambda(methodCall.Object, null);
    Delegate compiled = theTarget.Compile();

    return compiled.DynamicInvoke();    }

但是非常非常慢。


有没有更快的方法来获取方法调用表达式的目标?


对我的代码(GetDelegateDelegateCompileDelegateDynamicInvoke)以及 @IvanStoev 的代码(GetFuncFuncCompileFuncInvoke)进行基准测试会得出以下结果:

|                Method |           Mean |         Error |        StdDev |
|---------------------- |----------------|---------------|---------------|
|       DelegateCompile | 165,068.477 ns | 2,671.3001 ns | 2,498.7358 ns |
|           FuncCompile | 160,956.199 ns | 2,133.5343 ns | 1,995.7093 ns |
| DelegateDynamicInvoke |   1,148.191 ns |    11.7213 ns |    10.9642 ns |
|            FuncInvoke |       3.040 ns |     0.0264 ns |     0.0247 ns |

所以,Invoke 实际上比DynamicInvoke 快很多,但瓶颈实际上是Compile 调用。有没有办法不用编译表达式就可以得到target 对象?

基准代码:

public class Program
{
    private Delegate @delegate;
    private Func<object> func;

    private static Delegate GetDelegate(Expression<Action> expression)
    {
        MethodCallExpression methodCall = (MethodCallExpression) expression.Body;
        return Expression.Lambda(methodCall.Object, null).Compile();
    }

    private static Func<object> GetFunc(Expression<Action> expression)
    {
        MethodCallExpression methodCall = (MethodCallExpression) expression.Body;
        return Expression.Lambda<Func<object>>(methodCall.Object).Compile();
    }

    [GlobalSetup]
    public void Setup()
    {
        object o = new object();
        Expression<Action> expression = () => o.ToString();

        this.@delegate = Program.GetDelegate(expression);
        this.func = Program.GetFunc(expression);
    }

    [Benchmark]
    public void DelegateCompile()
    {
        object o = new object();
        Expression<Action> expression = () => o.ToString();

        Program.GetDelegate(expression);
    }

    [Benchmark]
    public void FuncCompile()
    {
        object o = new object();
        Expression<Action> expression = () => o.ToString();

        Program.GetFunc(expression);
    }

    [Benchmark]
    public void DelegateDynamicInvoke()
    {
        this.@delegate.DynamicInvoke();
    }

    [Benchmark]
    public void FuncInvoke()
    {
        this.func.Invoke();
    }

    public static void Main(string[] args)
    {
        BenchmarkRunner.Run<Program>();
    }
}

【问题讨论】:

  • 我认为Compile 是最慢的部分,但是Expression.Lambda&lt;Func&lt;object&gt;&gt;(methodCall.Object).Compile().Invoke() 怎么样
  • @IvanStoev 谢谢你的想法,但遗憾的是这也不能提高性能。基准测试显示187 us 用于我的问题中的代码,195 us 用于您提出的代码(这并没有更糟,但这不是我所希望的)。
  • 您在测量时是否将Compile()Invoke() 分开(如在相关代码中)?或者您正在测量整个方法?至少您可以看到DynamicInvokeInvoke 之间是否有区别。我仍然认为最慢的部分是 Compile() 方法,而不是调用(一种或另一种方式)。
  • @IvanStoev 请查看编辑。你说得对,Compile 实际上是最慢的部分。此外,您的方法比使用DynamicInvoke 快得多,但由于它使用Compile,它仍然很慢。不过还是谢谢你的帮助
  • 它应该说“获得赏金的答案会最佳地提出比问​​题中的代码工作(相当)快的代码”当然......

标签: c# performance delegates expression dynamic-invoke


【解决方案1】:

我能想到的避免耗费时间的Compile 操作的唯一方法是使用反射递归地评估表达式内容。

一般地执行此操作(处理所有案例)是一项复杂的任务。目前有超过 80 个ExpressionTypes,它们都具有不同的语义(嗯,有些属于具有相应基类的类别)。为了处理所有这些,可能应该创建自定义ExpressionVisitor 并实现评估引擎(可能带有某种评估堆栈)。

换句话说,很多工作/代码。

但是...如果我们将表达式限制为 2 种类型 - ConstantExpression(常量值)和 MemberExpression(常量值的字段或属性),那么有一个相对简单的解决方案。有问题的方法已经包含关于传递的Expression&lt;Action&gt; 的假设,并且示例表达式目标(它是一个闭包)属于常量值字段类别。

主要工作在私有递归方法中完成如下:

static object Evaluate(Expression expression)
{
    if (expression == null)
        return null;
    if (expression is ConstantExpression constExpression)
        return constExpression.Value;
    if (expression is MemberExpression memberExpression)
    {
        var target = Evaluate(memberExpression.Expression);
        if (memberExpression.Member is FieldInfo field)
            return field.GetValue(target);
        if (memberExpression.Member is PropertyInfo property)
            return property.GetValue(target);
    }
    throw new NotSupportedException();
}

使用它的方法是

public object GetExpressionTarget<T>(Expression<T> expression)
{
    var methodCall = (MethodCallExpression)expression.Body;
    return Evaluate(methodCall.Object);
}

我没有性能比较结果,但即使这是使用反射,它也应该比使用反射动态 IL 代码发射的Compile 快得多,不包括 @ 的创建987654330@ 和委托调用它。

【讨论】:

  • 伊万,非常感谢您的回答。该代码非常适合我的用例,并且比问题中的代码快得多 - 1,600 ns160,000 ns 相比。感谢您不仅提供完成任务的代码,还感谢您解释如何为所有表达式类型编写代码。你真的应该得到赏金:)
  • 在看到编译是有史以来最慢的事情之后,我开始写这篇文章,然后就像,我打赌有人已经为我做了这个!您应该扩展以包含 MethodCallExpression
【解决方案2】:

随着时间的推移,我对 Ivan 的解决方案进行了一些扩展,以防它对其他人有所帮助

        static object Evaluate(
            Expression expression
        )
        {
            switch (expression)
            {
                case ConstantExpression e:
                    return e.Value;

                case MemberExpression e when e.Member is FieldInfo field:
                    return field.GetValue(
                        Evaluate(
                            e.Expression
                        )
                    );

                case MemberExpression e when e.Member is PropertyInfo property:
                    return property.GetValue(
                        Evaluate(
                            e.Expression
                        )
                    );

                case ListInitExpression e when e.NewExpression.Arguments.Count() == 0:
                    var collection = e.NewExpression.Constructor.Invoke(new object[0]);
                    foreach(var i in e.Initializers)
                    {
                        i.AddMethod.Invoke(
                            collection,
                            i.Arguments
                                .Select(
                                    a => Evaluate(a)
                                )
                                .ToArray()
                        );
                    }
                    return collection;

                case MethodCallExpression e:
                    return e.Method.Invoke(
                        Evaluate(e.Object),
                        e.Arguments
                            .Select(
                                a => Evaluate(a)
                            )
                            .ToArray()
                    );

                default:
                    //TODO: better messaging
                    throw new NotSupportedException();
            }

        }

【讨论】:

    猜你喜欢
    • 2019-01-11
    • 1970-01-01
    • 2015-05-25
    • 1970-01-01
    • 1970-01-01
    • 2019-12-25
    • 2015-04-04
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多