代码改变世界

优化通过表达式树构造URL的性能

2009-09-01 19:29 Jeffrey Zhao 阅读(...) 评论(...) 编辑 收藏

我们继续改进通过表达式树构造URL的方式。在上一篇文章中,辅助方法可以正确地识别了ActionNameAttribute,而这次改进的则是性能方面的问题。首先还是来看一下用于从表达式树获取RouteValueDictionary的方法:

public static RouteValueDictionary GetRouteValues<TController>(
    Expression<Action<TController>> action)
    where TController : Controller
{
    ...

    var rvd = new RouteValueDictionary();
    rvd.Add("controller", controllerName);
    rvd.Add("action", GetActionName(call.Method));

    AddParameterValues(rvd, call);
    return rvd;
}

private static void AddParameterValues(
    RouteValueDictionary rvd, MethodCallExpression call)
{
    ParameterInfo[] parameters = call.Method.GetParameters();

    for (int i = 0; i < parameters.Length; i++)
    {
        Expression arg = call.Arguments[i];

        object value = null;
        ConstantExpression ce = arg as ConstantExpression;
        if (ce != null)
        {
            // If argument is a constant expression, just get the value
            value = ce.Value;
        }
        else
        {
            // Otherwise, convert the argument subexpression to type object,
            // make a lambda out of it, compile it, and invoke it to get the value
            Expression<Func<object>> lambdaExpression = 
                Expression.Lambda<Func<object>>(
                    Expression.Convert(arg, typeof(object)));
            Func<object> func = lambdaExpression.Compile();
            value = func();
        }

        rvd.Add(parameters[i].Name, value);
    }
}

这次我们关注的是第二个方法AddParameterValues。这个方法的目的是从表示action调用的表达式树(它是一个MethodCallExpression)中提取所有的参数——也是一个一个表达式树,并将它们表示的“值”填充到RouteValueDictionary中。这段代码使用了传统计算一个表达式树的方式:“使用LambdaExpression对象封装,再编译,最后执行”来获得一个Expression对象的值。但是,Compile方法的性能是比较低下的,如果密集地执行会对性能产生一定影响。

那么,您认为在ASP.NET MVC的场景中,Compile方法的执行频率如何呢?请想象一下这样的一个场景:

<h2>Article List</h2>
 
<% foreach (var article in Model.Articles) { %>
<div>
    <%= Html.ActionLink<ArticleController>(
            c => c.Detail(article.ArticleID, 1), article.Title) %>
    
    <% for (var page = 2; page <= article.MaxPage; page++) { %>
    <small>
        <%= Html.ActionLink<ArticleController>(
            c => c.Detail(article.ArticleID, page), page.ToString()) %>
    </small>
    <% } %>        
</div>
<% } %>

上述代码的作用,是在文章列表页上生成一系列指向文章详细页的链接。那么在上面的代码中,将会出现多少次表达式树的计算呢?

Html.ActionLink<ArticleController>(
    c => c.Detail(article.ArticleID, 1), article.Title)
 
Html.ActionLink<ArticleController>(
    c => c.Detail(article.ArticleID, page), article.Title)

可以看出,每篇文章将进行(2 * MaxPage – 1)次计算,对于一个拥有数十篇文章的列表页,计算次数很可能逾百次。此外,再加上页面上的各种其它元素,如分类列表,Tag Cloud等等,每生成一张略为复杂的页面便会造成数百次的表达式树计算。从Simone Chiaretta的性能测试上来看,使用表达式树生成链接所花时间,大约为直接使用字符串的30倍。而根据我的本地测试结果,在一台P4 2.0 GHz的服务器上,单线程连续计算一万个简单的四则运算表达式便要花费超过1秒钟时间。这并非是一个可以忽略的性能开销,引入一种性能更好的表达式树计算方法势在必行。

在《快速计算表达式树》一文中,我已经对这一话题进行了非常深入的探讨(甚至还可以算上表达式树的缓存这一系列文章),并最终给出了一个可以直接复用的解决方案:FastLambda。因此,我们在这里只需要使用如下的方式来改进表达式树的“计算”方式即可:

private static void AddParameterValues(
    RouteValueDictionary rvd, MethodCallExpression call)
{
    ParameterInfo[] parameters = call.Method.GetParameters();

    for (int i = 0; i < parameters.Length; i++)
    {
        rvd.Add(parameters[i].Name, Eval(call.Arguments[i]));
    }
}

private static FastEvaluator s_parameterEvaluator = new FastEvaluator();

private static object Eval(Expression exp)
{
    ConstantExpression ce = exp as ConstantExpression;
    if (ce != null) return ce.Value;

    return s_parameterEvaluator.Eval(exp);
}

FastEvaluator是FastLambda项目中提供的一个类,它会对输入的表达式树进行分析,并根据其结构缓存编译后的结果,于是下次输入结构相同的表达式时,便可以快速的计算出结果,省去了编译的开销。例如在上面的例子中,无论article指向的是什么对象,无论page的值是多少,article.ArticleID或page本身的结构永远是不变的。因此,对于刚才的例子,无论访问了多少次页面,作了多少次循环,都只会进行两次编译。此外,由于FastEvaluator已经实现为一个线程安全的组件,因此这里我们只须直接使用即可,无需进行太多考虑。

那么,这么做会带来多少性能提升呢?请看如下的示意图:

perf test

这幅图表达的是在计算拥有2n + 1个节点的表达式树时,普通的做法(红线)与FastEvaluator(紫线)的性能差距。根据我的个人经验,项目中所计算的表达式树的节点数量一般都在10个以内。如图所示,在这个数据范围内,FastEvaluator的计算耗时仅为传统方法的1/20。对于刚才的示例来说,节点数量为3或5,则表示n为1或3,在这种节点数量极少的情况下,性能的差距甚至可以达到50至100倍。

当然,一个应用程序的性能不是由这么简单的一个细节决定的,但是现在我们为一个较为频繁的操作进行了非常明显的性能优化。更重要的是,我们并没有因此损失任何易用性。因此,如果您在ASP.NET MVC中通过表达式树构造URL的话,我建议您使用现在的方案进行改进。

至于FastEvaluator组件的原理及实现等细节内容,还请参考我写的《快速计算表达式树》一文。