优化通过表达式树构造URL的性能
2009-09-01 19:29 by 老赵, 5495 visits我们继续改进通过表达式树构造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已经实现为一个线程安全的组件,因此这里我们只须直接使用即可,无需进行太多考虑。
那么,这么做会带来多少性能提升呢?请看如下的示意图:
这幅图表达的是在计算拥有2n + 1个节点的表达式树时,普通的做法(红线)与FastEvaluator(紫线)的性能差距。根据我的个人经验,项目中所计算的表达式树的节点数量一般都在10个以内。如图所示,在这个数据范围内,FastEvaluator的计算耗时仅为传统方法的1/20。对于刚才的示例来说,节点数量为3或5,则表示n为1或3,在这种节点数量极少的情况下,性能的差距甚至可以达到50至100倍。
当然,一个应用程序的性能不是由这么简单的一个细节决定的,但是现在我们为一个较为频繁的操作进行了非常明显的性能优化。更重要的是,我们并没有因此损失任何易用性。因此,如果您在ASP.NET MVC中通过表达式树构造URL的话,我建议您使用现在的方案进行改进。
至于FastEvaluator组件的原理及实现等细节内容,还请参考我写的《快速计算表达式树》一文。
写的天书呢?看不懂