当前位置: 首页 > 编程日记 > 正文

快速计算表达式树

前言

.NET 3.5中新增的表达式树(Expression Tree)特性,第一次在.NET平台中引入了“逻辑即数据”的概念。也就是说,我们可以在代码里使用高级语言的形式编写一段逻辑,但是这段逻辑最终会被保存为数据。正因为如此,我们可以使用各种不同的方法对它进行处理。例如,您可以将其转化为一个SQL查询,或者外部服务调用等等,这便是LINQ to Everything在技术实现上的重要基石之一。

实事求是地说,.NET 3.5中的表达式树的能力较为有限,只能用来表示一个“表达式”,而不能表示“语句”。也就是说,我们可以用它来表示一次“方法调用”或“属性访问”,但不能用它来表示一段“逻辑”。不过,微软在.NET 4.0中增强了这一特性。在.NET 4.0中,我们可以使用表达式树构建一段带有“变量声明”,“判断”,“循环”的逻辑。当“逻辑”成为“数据”时,我们就拥有了更广阔的空间来发挥创造力。例如,我们可以将一段使用C#编写的顺序型逻辑,转化为包含异步调用的客户端JavaScript代码,以此快速构建带有复杂客户端逻辑的Web应用程序。

不过,即便是.NET 3.5中表达式树的“半吊子”特性,也已经显著加强了.NET平台的能力,甚至改变了我们对于一些事物的使用方式。

表达式树的优势

详见文章完整内容(链接)。

表达式树的计算

对表达式树进行计算,是处理表达式树时中最常见的工作了。几乎可以这么说,任何处理表达式树的工作都无法回避这个问题。在这里,“表达式树的计算”是指将一个复杂的表达式树转化为一个常量。例如,下图中左侧的表达式树,便可以转化为右侧的常量。

请注意,右侧的结果是一个常量,而不是一个ConstantExpression对象。当然,我们在必要的时候,也可以重新构造一个ConstantExpression对象,以便组成新的表达式树供后续分析。这个例子非常简单,而在实际的使用过程中遇到的表达式往往会复杂的多,他们可能包含“对象构造”、“下标访问”、“方法调用”、“属性读取”以及“?:”三目运算符等各种成员。它们的共同点,便是继承于Expression这一基类,并且最终都可以被计算为一个常量。

传统的表达式树的计算方式,是将其进行Compile为一个强类型的委托对象并加以执行,如下:

Expression<Func<DateTime>> expr = () => DateTime.Now.AddDays(1);
Func<DateTime> tomorrow = expr.Compile();
Console.WriteLine(tomorrow()); 

如果是要计算一个类型不明确的表达式树,那么我们便需要要写一个通用的Eval方法,如下:

static object Eval(Expression expr)
{LambdaExpression lambda = Expression.Lambda(expr);Delegate func = lambda.Compile();return func.DynamicInvoke(null);
}static void Main(string[] args)
{Expression<Func<DateTime>> expr = () => DateTime.Now.AddDays(1);Console.WriteLine(Eval(expr.Body));
}

简单说来,计算表达式树的通用方法会分三步走:

  1. 将表达式树封装在一个LambdaExpression对象
  2. 调用LambdaExpression的Compile方法动态生成一个委托对象
  3. 使用DynamicInvoke方法调用该委托对象,获取其返回值

Compile方法在内部使用了Emit,而DynamicInvoke方法其本质与反射调用差不多,因此这种通用的表达式计算方法会带来相对较为可观的开销。尤其是在某些场景中,很容易出现大量表达式树的计算操作。例如,在开发ASP.NET MVC应用程序的视图时,“最佳实践”之一便是使用支持表达式树的辅助方法来构造链接,例如:

<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秒钟时间。这并非是一个可以忽略的性能开销,引入一种性能更好的表达式树计算方法势在必行。

减少Compile开销

详见文章完整内容(链接)。

减少反射开销

详见文章完整内容(链接)。

最后我们进行一个简单试验,将运算符数量为1-20的四则运算表达式各10个,分别计算1000次。三种实现耗时对比如下(请关注Normal和Fast的对比):

perf test

FastEvaluator的主要开销在于从ExpressionCache中提取数据,它随着表达式的长度线性增加。拥有n个运算符的四则运算表达式树,其常量节点的数量为n + 1,因此总结节点数量为2n + 1。根据我的个人经验,项目中所计算的表达式树的节点数量一般都在10个以内。如图所示,在这个数据范围内,FastEvaluator的计算耗时仅为传统方法的1/20,并且随着节点数量的减少,两者差距进一步增大。此外,由于节省了反射调用的开销,即使在CacheEvaluator可以正常工作的范围内(1-3个运算符),FastEvaluator相对前者也有明显的性能提升。

总结

表达式树拥有语义清晰,强类型等诸多优势,可以预见,越来越多的项目会采取这种方式来改进自己的API。在这种情况下,表达式树的计算对于程序性能的影响也会越来越大。本文提出了一种表达式树计算操作的优化方式,将不同表达式树“标准化”为几种有限的结构,并复用其编译结果。由于减少了编译操作和反射操作的次数,表达式计算所需开销大大降低。

本文所有代码都公布于MSDN Code Gallary中的FastLambda项目中,您可以根据需要随意修改使用。此外,FastLambda项目中还包含了可以将表达式树的多个常量部分进行简化的组件(如将5 + 2 + 3 * 4 * x简化为7 + 12 * x),这对于处理原本就包含ParameterExpression的表达式树非常有用(如编写LINQ Provider时)。如果您对此感兴趣,可以关注项目中的PartialEvaluator和FastPartialEvaluator类,它们的区别在于前者利用Evaluator,而后者利用FastEvaluator进行表达式树的局部计算。

全文链接

本文已发表于InfoQ中文站,欢迎访问《快速计算表达式树》。

转载于:https://www.cnblogs.com/JeffreyZhao/archive/2009/07/29/expression-tree-fast-evaluation.html

相关文章:

随手拈来尽是折劲额事体

昨天中午&#xff0c;justina同学请我去港丽吃饭&#xff0c;世界顿时美好了&#xff01; 猛地发现&#xff0c;港丽的酸菜鱼竟然非常好吃&#xff0c;除了价钱贵&#xff0c;基本没有缺点了。 吃饭的时候&#xff0c;看到两件有劲的事情&#xff0c;一件比一件更折劲&#xff…

06 面向对象之:反射,双下方法

一、反射 反射的概念是由Smith在1982年首次提出的&#xff0c;主要是指程序可以访问、检测和修改它本身状态或行为的一种能力&#xff08;自省&#xff09;。这一概念的提出很快引发了计算机科学领域关于应用反射性的研究。它首先被程序语言的设计领域所采用,并在Lisp和面向对象…

实现对学生信息的修改操作

返回目录&#xff1a;《学生信息管理系统&#xff08;JavaJSP&#xff09;》 本篇博客主要实现对学生信息的修改操作&#xff1b; 步骤1、在学生信息的显示页面&#xff08;即student.jsp页面&#xff09;中&#xff0c;在表格最后增加一列“修改”超链接&#xff0c;在<tr&…

UML用例图概要(转)

用例图主要用来图示化系统的主事件流程&#xff0c;它主要用来描述客户的需求&#xff0c;即用户希望系统具备的完成一定功能的动作&#xff0c;通俗地理解用例就是软件的功能模块&#xff0c;所以是设计系统分析阶段的起点&#xff0c;设计人员根据客户的需求来创建和解释用例…

Python之迭代器,生成器与装饰器

1》迭代器原理及使用&#xff1a; 1>原理&#xff1a; 迭代器是访问集合元素的一种方式&#xff0c;迭代器对象从集合的第一个元素开始访问&#xff0c;直到所有的元素被访问完结束&#xff1b;迭代器只能往前不会后退&#xff0c;不过这也没什 么&a…

像乔布斯一样演讲

当苹果公司CEO史蒂夫-乔布斯开始今年&#xff08;2008年1月&#xff09;的Macworld时&#xff0c;我们看到他的商业演讲&#xff08;以下简称&#xff1a;演讲&#xff09;水平更上了一层楼。所有人都希 望能够在演讲中能更加简明扼要&#xff0c;乔布斯做到了&#xff0c;并且…

UNICODE使用的一些知识和技巧

UNICODE宏和_UNICODE宏的关系 在windows编程中,经常要编译Unicode版本的程序,方法是工程文件的配置中加上UNICODE或者_UNICODE编译条件,那么到底是用哪一个呢? Jeffrey Richter在《Windows核心编程》中说,_UNICODE宏用于C运行期头文件,而UNICODE宏则用于Windows头文件.当编译源…

编程学习网站收集

目录 1. 菜鸟教程 1.1 Java 教程 1.2 HTML 教程 1.3 CSS 教程 1.4 JavaScript 教程 1.5 JSP 教程 1.6 Servlet 教程 1.7 jQuery 教程 1.8 AJAX 教程 1.9 MySQL 教程 2. 易百教程 3. w3school 在线教程 1. 菜鸟教程 菜鸟教程 (www.runoob.com) 提供了编程的基础技术…

js短路表达式

今天碰见个题目&#xff0c;感觉短路表达式很好用。 题目&#xff1a; 定义一个计算圆面积的函数area_of_circle()&#xff0c;它有两个参数&#xff1a;r: 表示圆的半径&#xff1b;pi: 表示π的值&#xff0c;如果不传&#xff0c;则默认3.14function area_of_circle(r, pi) …

51nod 1065 最小正字段和 解决办法:set存前缀和,二分插入和二分查找

题目&#xff1a; 这题要求大于0的最小字段和&#xff0c;常规O&#xff08;n&#xff09;求最大字段和的方法肯定是没法解的。 我的解法是&#xff1a;用sum[i]存前i项的和&#xff0c;也就是前缀和。 这题就变成了求sum[j]-sum[i]的大于0的最小值( j > i )。 我们可以看到…

著名作者网站论文下载

http://www.cs.wright.edu/~agoshtas/ardyCV.htmlhttp://www.cse.msu.edu/~stockman/http://www.dkfz.de/tbi/people/homepages/rohr/转载于:https://www.cnblogs.com/stoneresearch/archive/2008/11/06/4336753.html

内存与进程管理

这一节的内容有点杂&#xff0c;只能自己手动输入了 1.uname命令用于打印当前系统相关信息&#xff08;内核版本号、硬件架构、主机名称和操作系统类型等&#xff09;。 uname -a显示全部信息 2.cat /etc/redhat-release 查看当前系统版本 3.free -m / -g 查看内存使用状况&…

Python的闭包和装饰器

什么是闭包 python中函数名是一个特殊的变量&#xff0c;它可以作为另一个函数的返回值&#xff0c;而闭包就是一个函数返回另一个函数后&#xff0c;其内部的局部变量还被另一个函数引用。 闭包的作用就是让一个变量能够常驻内存。 def count():fs []for i in range(1, 4):de…

用@Data注解的形式替代类中的setter、getter方法

目录 1. 封装 2. Data注解介绍 3. Lombok的使用 1. 封装 在类中&#xff0c;为了增强数据的安全性和隐蔽性&#xff0c;通常会对数据和与数据有关的方法进行封装&#xff1b; 封装的步骤&#xff1a; 1、将类中的属性设置为private(私有的)&#xff0c;只能本类才能访问&…

note-在VisualStudio中使用正则表达式

前言&#xff1a;本来昨天已经写了&#xff0c;但由于意外给搞丢失了&#xff0c;由于刚刚看了这篇文章知道了一些真相&#xff1b;现在的心理状态已经和昨天不一样了&#xff0c;昨天是满心的高兴&#xff0c;对VisualSduio很有好感&#xff0c;当时自认为是没有把正则学好&am…

AS 发展小计

2000 -- 2003: ActionScript 1.0, 和 Flash 5.0 一起发布&#xff1b;变成可文本编辑&#xff08;以前是从对话框和下拉菜单中选择&#xff0c;当然&#xff0c;那个时候不叫AS&#xff09;添加 switch 语句和strict equality 操作&#xff1b;2003 -- 2006: ActionScript 2.0 …

2019年3月8日比赛(知网是什么)

第一题&#xff08;对冒泡排序原理的理解&#xff09; 题意&#xff1a;第一行的输入代表下一行输入的无序数的数的个数&#xff0c;然后下一行&#xff0c;数字与上一行数字对应&#xff0c;若对应为1则该数可以与下一个数交换位置。 根据冒泡排序可知&#xff0c;任何一个无序…

Mybatis 中$与#的区别

1 #是将传入的值当做字符串的形式&#xff0c;eg:select id,name,age from student where id #{id},当前端把id值1&#xff0c;传入到后台的时候&#xff0c;就相当于 select id,name,age from student where id 1. 2 $是将传入的数据直接显示生成sql语句&#xff0c;eg:select…

UUID.randomUUID()生成唯一识别码

目录 1、UUID 的概念 2、UUID的组成 3、UUID.randomUUID()使用 1、UUID 的概念 UUID&#xff08;Universally Unique Identifier&#xff09;&#xff1a;通用唯一识别码&#xff0c;是一种软件建构的标准。 UUID 目的是让分布式系统中的所有元素&#xff0c;都能有唯一的…

人生应该记住的16句话(转载)

1、再烦&#xff0c;也别忘微笑&#xff1b;再急&#xff0c;也要注意语气&#xff1b; 再苦&#xff0c;也别忘坚持&#xff1b;再累&#xff0c;也要爱自己。 2、 低调做人&#xff0c;你会一次比一次稳健&#xff1b;高调做事&#xff0c;你会一次比一次优秀。 3、 成功的时…

转:一个简单的基于WEB的QTP自动化测试框架-SAFFRON

来源: http://www.itestware.com/ctest/index.php?optioncom_content&viewarticle&id62:webqtp-saffron&catid35:testing_is_believing 06年的时候&#xff0c;Mercury 提供了一个QTP自动化测试框架原型 - SAFFRON&#xff0c;可用于指导我们开发基于WEB的自动化测…

积极学习的朋友

自从去年7月反省之后&#xff0c;认识的朋友逐渐多了&#xff0c;天下那么大&#xff0c;优秀的人很多&#xff0c;通过网络认识是一个很不错的途径&#xff0c;经过一段时间后&#xff0c;圈子范围扩大了很多&#xff0c;行业上和非行业上都有涉及&#xff0c;对自己认知冲击很…

hive向表格中插入数据并分析语句

1&#xff0c;---导入mds_imei_month_info set hive.exec.max.dynamic.partitions 100000; //最大的动态分区表 set hive.support.concurrencyfalse; //是否支持并发 set hive.exec.max.dynamic.partitions.pernode 100000; //each mapper or reducer可以创建的最大动态分区数 …

资源与工具下载

Red Hat Linux镜像文件 链接&#xff1a;https://pan.baidu.com/s/1N3Khh6pyKpMkOnkEL-U9_A 提取码&#xff1a;0hnu jdk1.8(32位64位)安装版 链接&#xff1a;https://pan.baidu.com/s/15Jm6Ca6IBR3OEauge-4FYQ 提取码&#xff1a;uo2l Tomcat 8(32位64位)安装版 链接…

Global.asax中Application_Error无法执行

Global.asax中Application_Error无法执行问题解决后才发现这句是错误的&#xff0c;之前用VS2005开发后发布到服务器上也出现这种情况&#xff0c;后来莫名 的好了(是解决了没发现原因)。之前的文章链接后来用catch捕捉后真相大白&#xff0c;System.Security.SecurityExceptio…

虚拟化如何做实?详解戴尔2.0版解决方案

继5月份推出虚拟化解决方案后&#xff0c;2008年9月16日&#xff0c;戴尔又宣布推出包括新款服务器、存储产品&#xff0c;管理工具及基础设施咨询服务在内的全新虚拟化2.0解决方案&#xff0c;致力于为客户提供通向虚拟化的智慧之道。戴尔的虚拟化解决方案包括哪些具体的内容&…

Socket/ServerSocket 选项

Socket/ServerSocket 选项 原文:Socket/ServerSocket 选项在网络编程中&#xff0c;Socket/ServerSocket有一些选项用来自定义一些行为&#xff0c;现在分享一下。Socket选项 1.TCP_NODELAY 在Socket发送数据时&#xff0c;默认情况下&#xff0c;数据会先进入缓冲区&#xff0…

MySQL主从复制的常用拓扑结构

1、复制的常用拓扑结构 复制的体系结构有以下一些基本原则&#xff1a; (1) 每个slave只能有一个master&#xff1b; (2) 每个slave只能有一个唯一的服务器ID&#xff1b; (3) 每个master可以有很多slave&#xff1b; (4) 如果你设置log_slave_updates&#xff0c;…

统计java文件中的代码行数

统计Java代码行数工具类 —— CodeCounterUtil.java 统计指定目录下的java文件中代码行数 —— public static int getCodeNumFromFolder(String filePath)统计具体文件中的java代码行数 —— public static int getCodeNumFromFile(String filePath)import java.io.Buf…

jQuery插件thickbox在ie下垂直居中问题

jQuery 插件 thickbox 3.1 在ie下总不能垂直居中&#xff0c;按“http://jamazon.co.uk/web/2008/03/17/thickbox-31-ie7-positioning-bug/”上的方法改了也没用&#xff0c;咋办&#xff1f; 我是这样改的&#xff1a;管它ie6还是ie789&#xff0c;一视同仁&#xff01; 原284…