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

所以,上周我一直在找一个小型、实用的计算数学表达式的类库。偶然间我在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. 巧用PDF转Markdown插件,在扣子(Coze)手搓一个有趣好玩的AI Bot

    近期,TextIn团队开发的PDF转Markdown插件已经上架Coze平台. 短短的时间内,已经有不少朋友愉快地和我们的工具开始玩耍.今天我们抛砖引玉,介(an)绍(li)几种PDF转Markdow ...

  2. Angular 16+ 高级教程 – 谈谈 ASP.NET Core & Angular & React 在业务开发上各自的优势和体验

    前言 日常, 我的开发都围绕着 ASP.NET Core 和 Angular. 这篇想聊聊它们各自的特点和解决问题的方式. 以及最重要的, 我们该在什么时候采用何种方案更为妥当. 浅谈项目分类 我一般 ...

  3. k8s 中的 Ingress 简介

    〇.前言 前边已经介绍了 k8s 中的相关概念和 Service,本文继续看下什么是 Ingress. Ingress 的重要性不言而喻,它不仅统一了集群对外访问的入口,还提供了高级路由.七层负载均衡 ...

  4. k8s StorageClass 存储类

    目录 一.概述 1.StorageClass 对象定义 2.StorageClass YAML 示例 二.StorageClass 字段 1.provisioner(存储制备器) 1.1.内置制备器 ...

  5. 完美转发(模板)--T&&

    在C++模板编程中,完美转发(Perfect Forwarding)是一种技术,旨在保留函数参数的值类别,即在将参数传递到另一个函数时,无论参数是左值还是右值,都能够保持它的原始性质,而不会因为转发丢 ...

  6. `std::string_view`(c++17) 和 `std::stringstream` 使用区别·

    std::string_view 和 std::stringstream 都是 C++ 中处理字符串的工具,但它们的设计目标和使用场景非常不同.我们可以通过几方面进行对比. 1. 设计目的和核心功能 ...

  7. PasteForm最佳CRUD实践,实际案例PasteTemplate详解之3000问(四)

    无论100个表还是30个表,在使用PasteForm模式的时候,管理端的页面是一样的,大概4个页面, 利用不同操作模式下的不同dto数据模型,通过后端修改对应的dto可以做到控制前端的UI,在没有特别 ...

  8. 利用3Dslice提取血管中心线

    1.首先进入官网下载你需要的版本.你也可以安装老版本,我已经用红色框框出来了. 2.开始安装,等个几十秒钟就ok了. 3.当然要实现提取中心线,还需要 VMTK 这个玩意, 打开应用,找到 insta ...

  9. 前端 export default 和 export const name 的区别?

    export default 是默认导出export const 是命名导出 在一个vue文件中export const可以有多个,但是export default只有且仅有一个,{}表示导入非默认变 ...

  10. 77.const声明对象修改对象里面的值会触发报错吗

    不会,因为对象是复杂类型数据 :对象的地址保存在栈内存中,对象的数据保存在堆内存中 : 只要对象的地址不发生改变,无论堆内存的对象数据如何改变,对象的值就不会改变 :