当然,标题有点吸引人,但确实如此(您当然不相信自己没有伪造自己的基准,但这是另一回事了)。
因此,上周我正在寻找一个小型且可用的库来评估数学表达式。 我几乎直接偶然发现了这个stackoverflow帖子 。 推荐的库( Expr )确实非常快,几乎满足了我的所有需求。 但是,它没有提供限制变量范围的功能(一切都在VM内的一个全局命名空间中)。
因此,我做到了,通常这是我们不应该做的:我重新发明了轮子,并编写了自己的解析器/评估器。 无论如何,这是一个下雨的星期六,所以我认为一个小的递归降序解析器,一个可以简化并最终计算表达式以及一个用于管理变量的小助手的AST似乎并不重要。 事实并非如此。 我有一个初步的实现,并运行得非常快。 一旦进行了一些测试,使我确信它可以正确地计算所有内容,我想知道与原始文章中提到的其他库相比,评估器的运行速度。 由于没有手动优化每个内部循环和所有内容,因此我没有太大期望,毕竟有些库还是商业库。 因此,当我查看结果时,我感到非常惊讶。 下面的列表显示了一个微型基准,该基准使用相应的库评估相同的表达。 我的库parsii的测量是使用最终版本完成的,该版本执行了一些简化操作,例如预先评估常量表达式。 但是,没有像字节码生成之类的“黑魔法”或该联盟中的任何事情完成。
对于性能测量,表达式x为(2 +(7 – 5)* 3.14159 * x ^(12-10)+ sin(-3.141)”,其中x从0到1000000。对JIT进行10次加热。然后再执行15次,平均执行时间为:
- PARSII :28.3毫秒
- 曝光 :37.2毫秒
- MathEval :7748.5毫秒
- JEP :647.0毫秒
- MESP :220.8毫秒
- JFEP :274.3毫秒
现在,我敢肯定,这些库中的每一个都有各自的优势,因此无法直接进行比较。 看到一个简单的实现可以很好地竞争仍然令人惊奇。
对于那些不太了解编译器构造的人,下面简要介绍一下它的工作原理:
与任何解析器或编译器一样,parsii使用经典的方法是使用分词器 ,该工具将字符流转换为令牌流。 因此,作为字符数组的“ 4”,“”,“ +”,“”,“ 3”,“”,“ *”,“ 8”的“ 4 + 3 * 8”将被转换为:
- 4(整数)
- +(符号)
- 3(整数)
- *(符号)
- 8(整数)
令牌生成器查看当前字符,然后确定要查看的令牌类型,然后读取属于该令牌的所有字符。 每个标记都有其类型,文本内容,并且知道其起始位置(行和字符)。 网上有很多深入的教程,因此这里不再赘述。 您可以看一下源代码,但是正如我所说的,它只是一个简单的基本实现。
解析器将经典的递归降序解析器转换为AST(抽象语法树),然后可以对其进行评估。 这是构建解析器的最简单方法之一,因为它完全是手工编写的,而不是由工具生成的。 这样的解析器基本上包含每个语法规则的方法。
再次有很多此类解析器的教程。 但是,大多数示例遗漏的是正确的错误处理。 除了正确,快速地解析表达式之外,良好的错误处理是良好的解析器的主要方面之一。 这并不难:正如您在源代码中所看到的那样,解析器在解析表达式时从不抛出异常。 将收集所有错误,并且解析器将继续尽可能长的时间。 即使在出现第一个错误之后,仍无法正确评估生成的AST,但请务必继续操作,并且应该一次报告尽可能多的错误,这一点很重要。 令牌生成器使用相同的方法,因为将格式不正确的令牌(例如带有两个小数分隔符的十进制数字)报告给同一错误列表。
评估AST是解析表达式的结果,这非常容易。 语法树的每个节点都有一个评估方法,该方法将由其父节点从根节点开始调用。 此处eval的结果是对表达式求值的结果。 可以在BinaryOperation中找到这种方法的基本示例,它表示+,-,*等操作。
为了稍微缩短评估时间,执行了三个优化:
首先,在解析AST之后,在根节点上调用一种称为simple的方法,该方法会传播到每个子节点。 然后,每个节点决定是否可以找到自己的子表达式的更简单表示形式。 例如:对于二进制运算 ,我们检查两个操作数是否都是常数(数字)。 在这种情况下,我们对表达式求值并返回包含操作结果的新常量。 对于所有参数都恒定的函数,也可以这样做。
在表达式中使用变量时完成第二次优化。 幼稚的方法是使用映射并在需要时读取或写入变量的值。 尽管这确实可行,但是在执行时需要进行很多查找。 因此,我们有一个名为Variable的特殊类,其中包含变量的名称和数值。 解析表达式时,将在范围(基本上只是一个映射)中查找一次该变量,然后从现在开始使用。 由于每个查找返回相同的实例,因此在评估表达式时对变量的访问与对字段的读取或写入一样便宜,因为我们仅访问Variable的value字段。
第三次也是最后一次优化可能不会经常发挥作用。 但由于易于实现,因此还是可以实现。 它基本上被称为“惰性求值”,并在调用函数时使用。 函数不会自动求值其所有参数,然后自动执行函数调用。 它宁可查看参数,也可以凭自己决定要评估的参数,而不是要决定的参数。 在if函数中可以找到使用它的示例。
parsii是根据MIT许可获得许可的。 可以在GitHub上找到所有源代码以及预编译的jar。
翻译自: https://www.javacodegeeks.com/2014/01/how-to-write-one-of-the-fastest-expression-evaluators-in-java.html