当然,这个标题是有一点夺人眼球,但我确实这么做了(关于是否相信基准测试结果,这是另一个话题)。

所以,上周我一直在找一个小型、实用的计算数学表达式的类库。偶然间我在stackoverflow上看到了一个帖子,里面推荐的库(Expr)确实是很快而且基本拥有我需要的所有特性。但不幸的是,它不支持提供限制变量范围(在虚拟机里面,所有变量都位于一个全局命名空间)。

所以,我做了一件正常人不会做的事情:重新发明轮子,自己编写一个解析器和执行器。那是一个下雨的周六,我想到了用一个小型递归向下的解析器,一个简化了的、可以计算表达式的抽象语法树。抽象语法树使用一个小型变量管理助手,看起来也没什么大不了的。但它不是没有用。我做出一个初步的实现并且执行速度特别快。在进行了一些测试后,更让我充满信心,它执行的所有运算都正确无误。我想与最前面提到的类库相比,确认这个计算器到底有多快。在没有对每个内部循环和其他的执行进行优化前,我不报太大的期望,毕竟有不少类库是商业软件。所以当我看到测试结果的时候很惊讶。下面的清单展示了一个小的基准测试,使用不同的类库计算同一个表达式。 parsii 是我编写的库,测试时用的是最终版本。这个版本做了一些简化,比如预先计算了常量表达式。但是没有使用任何“黑魔法”,比如生成字节码或者其他类似的操作。

在性能评估中,一个用例是执行表达式”2 + (7 – 5) * 3.14159 * x^(12-10) + sin(-3.141)”。其中X的取值范围为0到1000000。测试时先运行10次,对JIT进行预热。然后再运行15次计算平均时间:

  • PARSII: 28.3 ms
  • EXPR: 37.2 ms
  • MathEval: 7748.5 ms
  • JEP: 647.0 ms
  • MESP: 220.8 ms
  • JFEP: 274.3 ms

现在我敢肯定,每一个类库都有自己的优势,所以不能直接对它们进行比较。尽管如此,令人吃惊的是一个简单实现的程序可以拥有这么好的表现。

如果读者对于编译器的原理不太了解的话,下面是一个关于编译器运行机制的简单介绍:

同其他的解析器或者编译器一样,parsii使用了传统的分词器。它将字符流转化成词法单元流,所以”4
+ 38″,也就是字符数组’4′, ‘ ‘, ‘+’, ‘ ‘, ’3′ , ‘ ‘, ‘‘, ’8′被转化成:

  • 4 (整数)
  • + (符号)
  • 3 (整数)
  • * (符号)
  • 8 (整数)

分词器取到一个字符,接着判断是一个什么类型的词法单元,然后再读入这个属于词法单元的所有字符。每一个词法单元都有类型、文本内容并且知道起始位置(行号和字符)。网上有很多深入的教程,所以在这里就不详细讲解了。你可以看一下源代码,但正如我说的,它只是一个初步的实现。

解析器用来将传入的词法单元流翻译成可以执行的AST(抽象语法树),它是一个传统的自上而下递归解析器。这是实现解析器最简单的方式,完全手写,没有利用工具生成。像这样的解析器只拥有一个包含所有语法规则的方法。

同样,关于这种类型的解析器也有很多的教程,但是如何恰当地处理错误却缺少相关的示例。除了解析表达式的速度和正确性外,优秀的错误处理机制是一个优秀解析器的最核心因素之一。正如在源代码里看到的那样,实现起来并不是太困难。因为解析器在解析表达式的过程中从来不会抛出异常,

所有的错误都被收集起来,并且继续尽可能进行解析。即使在第一个错误发生以后已经不能成功解析生成AST,重要的是要能够尽可能的继续解析。因为在一次的执行中我们需要报告尽可能多的错误。这样的方法也同样用在了分词器报告上。比如报告非法格式的词法单元,例如带有2个小数点的浮点数,放到同样的错误列表中。

执行一个解析完成后的抽象语法树非常简单。每一个抽象语法树节点都包含一个计算方法,从根节点开始到父节点会调用这个它。这里的执行结果就是表达式的结果,一个简单的例子就是算数运算,包含了+、-、*等操作。

执行一个解析完成后的抽象语法树非常简单。每一个抽象语法树节点都包含一个计算方法,它的父亲从根节点开始调用此方法。算数运算,代表了+、-、*等操作。

为了减少执行时间,程序里运用了3种优化措施:首先,在完成解析AST后,在根节点上进行一个简化的方法调用,并且会扩散到每一个子节点。每一个节点判断自己的子表达式中是否有简化的表达形式。例如:对于算数运算,我们检查2个操作数是不是都是常量(数字)。如果是数字,我们将计算表达式并且返回一个包含计算结果的常量。对于函数,如果所有的参数都是常量的话,也会进行此类优化。

在表达式中使用变量时会执行第二种优化。这里使用map用来在需要的时候来对变量的值进行读写。这肯定是有效的,并且会进行很多次的查找。所以我们有一个叫做Variable类,它包含了变量名称和变量值。在进行表达式解析时,变量在作用域范围内(仅是一个map)只被查找一次,之后就可以一直使用。由于每次查找都返回相同的实例,所以在计算表达式值时变量的访问就像读写字段一样廉价,因为我们刚刚获取了Variable类的

第三个也是最后一个优化很可能不是经常起作用。但是由于易于实现,还是应用了这种尤华。它的功能基本上和名字“延迟运算”一样,主要用于函数调用。函数不会自动计算所有参数值,并且调用函数。而“延迟运算”会检查所有的参数,自行决定哪些参数需要计算。在if函数中可以看到它应用的实例。

parsii遵循MIT许可证授权。在GitHub上可以找到所有的源代码,并且包含了预编译的jar包。

原文链接: dzone 翻译: ImportNew.comlumeng689
译文链接: http://www.importnew.com/9679.html

如何编写一个高效的Java表达式求值程序的更多相关文章

  1. C++表达式求值(利用数据结构栈)

    唉,刚刚用C++又又一次写了一个较完好的表达式求值程序,最后精简后程序还不到100行.这不经让我 想到了大一上学期刚学c语言时自己费了好大的劲,写了几百行并且功能还不是非常齐全(当时还不能计算有括号的 ...

  2. Aviator 表达式求值引擎开源框架

    简介¶ Aviator是一个高性能.轻量级的java语言实现的表达式求值引擎,主要用于各种表达式的动态求值.现在已经有很多开源可用的java表达式求值引擎,为什么还需要Avaitor呢? Aviato ...

  3. 表达式求值(noip2015等价表达式)

    题目大意 给一个含字母a的表达式,求n个选项中表达式跟一开始那个等价的有哪些 做法 模拟一个多项式显然难以实现那么我们高兴的找一些素数代入表达式,再随便找一个素数做模表达式求值优先级表 - ( ) + ...

  4. java实现表达式求值 (20 分)-------非递归版

    Dr.Kong设计的机器人卡多掌握了加减法运算以后,最近由学会了一些简单的函数求值.比如,它知道函数min(20, 23)的值是20, add(10, 98)的值是108等等.经过训练,Dr.Kong ...

  5. java实现算术表达式求值

    需要根据配置的表达式(例如:5+12*(3+5)/7.0)计算出相应的结果,因此使用java中的栈利用后缀表达式的方式实现该工具类. 后缀表达式就是将操作符放在操作数的后面展示的方式,例如:3+2 后 ...

  6. 奇怪的表达式求值 (java实现)

    题目参考:http://blog.csdn.net/fuxuemingzhu/article/details/68484749 问题描述; 题目描述: 常规的表达式求值,我们都会根据计算的优先级来计算 ...

  7. 蓝桥杯算法训练 java算法 表达式求值

    问题描述 输入一个只包含加减乖除和括号的合法表达式,求表达式的值.其中除表示整除. 输入格式 输入一行,包含一个表达式. 输出格式 输出这个表达式的值. 样例输入 1-2+3*(4-5) 样例输出 - ...

  8. Java描述表达式求值的两种解法:双栈结构和二叉树

    Java描述表达式求值的两种解法:双栈结构和二叉树 原题大意:表达式求值 求一个非负整数四则混合运算且含嵌套括号表达式的值.如: # 输入: 1+2*(6/2)-4 # 输出: 3.0 数据保证: 保 ...

  9. [Java]算术表达式求值之三(中序表达式转二叉树方案 支持小数)

    Entry类 这个类对表达式的合法性进行了粗筛: package com.hy; import java.io.BufferedReader; import java.io.IOException; ...

  10. [Java]算术表达式求值之二(中序表达式转后序表达式方案,支持小数)

    Inlet类,入口类,这个类的主要用途是验证用户输入的算术表达式: package com.hy; import java.io.BufferedReader; import java.io.IOEx ...

随机推荐

  1. MRI roi图像合并

    笔记来源:MRI roi的图像合并 dpabi小工具_哔哩哔哩_bilibili 1. 如果几个图像的维度不一致,需要先进行reslice 1)如何看图像的维度 以软件MRIcron为例, windo ...

  2. 【YashanDB数据库】YashanDB如何回收表空间

    确认表空间高水位线,是否有可回收空间. select a.tablespace_name,a.high_water_mark,b.user_bytes,b.total_bytes from (SELE ...

  3. 剖析 Redis List 消息队列的三种消费线程模型

    Redis 列表(List)是一种简单的字符串列表,它的底层实现是一个双向链表. 生产环境,很多公司都将 Redis 列表应用于轻量级消息队列 .这篇文章,我们聊聊如何使用 List 命令实现消息队列 ...

  4. OIDC – 关于 SAML 和 ADFS

    前言 最近听朋友说, 正在使用 ADFS 做 SSO, 哎哟. 我都是用 OIDC 做的, 这个 ADFS 是啥? 没听过. 于是搜索了一下, 这里先做一个小记入. ADFS 介绍 参考: stack ...

  5. HTML – script async defer

    参考 Youtube – #3 JavaScript Loading Strategies (async and defer) | JavaScript Full Tutorial no async ...

  6. ASP.NET Core – Byte, Stream, Directory, File 基础

    前言 以前的文章: Stream 基础和常用 和 IO 常用. 这篇主要是做一个整理, 方便要用的时候 warm up. 之前有讲过 Bit, Byte 的基本概念: Bit, Byte, ASCII ...

  7. CMake构建学习笔记16-使用VS进行CMake项目的开发

    目录 1. 概论 2. 详论 2.1 创建工程 2.2 加载工程 2.3 配置文件 2.4 工程配置 2.5 调试执行 3. 项目案例 4. 总结 1. 概论 在之前的系列博文中,我们学习了如何构建第 ...

  8. Spring —— bean生命周期

    bean生命周期 生命周期:从创建到消亡的完整过程 bean生命周期:bean从创建到销毁的整体过程 bean生命周期控制:在bean创建后到销毁前做一些事情   方式一:配置控制生命周期 <b ...

  9. C++指针等于地址加偏移量

    概述 本文通过c++示例代码演示指针的加减法运算及对 "指针 = 地址 + 偏移量" 的理解. 研究示例 1. 首先来检查各种变量类型所占的内存大小 #include <io ...

  10. Android系统之System Server大纲

    前言 System Server是android 基本服务的提供者,是android系统运行的最基本需求,所有server运行在一个叫system_process的进程中,system_process ...