我正在编写一个用于编译成JS的模板语言的解析器(如果这是相关的)。我从一些简单的正则表达式开始,这似乎有效,但正则表达式非常脆弱,所以我决定编写一个解析器。我开始编写一个简单的解析器,通过推送/弹出堆栈来记住状态,但事情一直在升级,直到我手上有一个递归下降解析器。
不久之后,我比较了以前所有解析方法的性能。递归下降解析器到目前为止最慢。我被困了:是否值得使用递归下降解析器来处理简单的事情,或者我是否有理由采取快捷方式?我很想去纯粹的正则表达式路线,这种路线非常快(几乎比RD解析器快3倍),但在某种程度上非常黑客且无法维护。我认为性能不是非常非常重要,因为编译模板是缓存的,但递归下降解析器是每个任务的正确工具吗?我想我的问题可以被视为更具哲学性的问题:在多大程度上牺牲性能的可维护性/灵活性是值得的?
答案 0 :(得分:6)
递归下降解析器可以非常快。
这些通常用词法分析器组织,它使用正则表达式来识别提供给解析器的语言标记。处理源文本的大部分工作都是由词法分析器逐个字符完成的,使用RE经常编译成的疯狂快速的FSA。
解析器只会偶尔看到标记与词法分析器看到字符的速率相比较,因此它的速度通常无关紧要。然而,当比较解析器到解析器的速度,忽略lex令牌所需的时间时,递归下降解析器可以非常快,因为它们使用函数调用实现解析器堆栈,与一般解析器push-current-state-相比,函数调用已经非常有效上模拟的堆栈。
所以,你可以吃蛋糕,也可以吃。使用regexps作为词位。使用解析器(任何类型,递归下降都很好)来处理词位。你应该对表现感到满意。
这种方法也满足了其他答案所做的观察:以使其可维护的方式编写它。我保证,Lexer / Parser分离非常好。
答案 1 :(得分:0)
可读性优先,后期表现......
因此,如果您的解析器使代码更具可读性,那么它就是正确的工具
答案 2 :(得分:0)
在多大程度上值得牺牲 可维护性/灵活性 性能
我认为将清晰的可维护代码作为第一优先级非常重要。在您的代码不仅表明它是瓶颈,而且您的应用程序性能也受到影响之前,您应始终将清晰的代码视为最佳代码。
不重新发明轮子也很重要。看看另一个解析器的评论非常好。通常会找到用于编写例程的常用解决方案。
当应用于适用的东西时,重复是非常优雅的。在我自己的实验中,由于递归导致的慢代码是一个例外,而不是常态。
答案 3 :(得分:0)
递归下降解析器应该更快
......或者你做错了什么。
首先,您的代码应分为两个不同的步骤。 Lexer + Parser。
在线的一些参考示例首先将整个语法首先标记为大型中间数据结构,然后将其传递给解析器。虽然有益于示范;不要这样做,它会使时间和内存复杂性增加一倍。相反,只要词法分析器确定匹配,就通知解析器状态转换或状态转换+数据。
至于词法分析者。这可能是您找到当前瓶颈的地方。如果词法分析器与解析器完全分离,您可以尝试在Regex和非Regex实现之间进行抖动以比较性能。
无论如何,正则表达式并不比读取原始字符串更快。它只是避免了一些常见的错误。具体来说,不必要地创建字符串对象。理想情况下,您的词法分析器应扫描您的代码并生成一个零中间数据的输出,除了在解析器中跟踪状态所需的最低限度。在记忆方面你应该:
例如,如果您当前的词法分析器与非终端匹配并逐个复制每个字符,直到它到达下一个终端;你实际上是为匹配的每个字母重新创建字符串。请记住,字符串数据类型是不可变的,concat将始终创建一个新字符串。您应该使用指针算术或某些等效的方法扫描文本。
要解决此问题,您需要从非终端的startPos扫描到非终端的末尾,并仅在匹配完成时进行复制。
Regex默认支持所有这一切,这就是为什么它是编写词法分析器的首选工具。而不是尝试编写一个解析整个语法的正则表达式,而是编写一个仅关注匹配终端的正则表达式。非终端作为捕获组。跳过标记化,并将结果直接传递到解析器/状态机。
这里的关键是,不要试图将Regex用作状态机。它最多只适用于Regular(即Chomsky Type III,无堆栈)声明性语法 - 因此称为Regular Expression。例如,HTML是一种无上下文(即基于Chomsky Type II,基于堆栈)的声明性语法,这就是为什么仅Rexeg永远不足以解析它。您的语法以及通常所有模板语法都属于这一类。你已经明显达到了Regex的极限,所以你走的正确。
仅使用Regex进行标记化。如果您真的关心性能,请重写词法分析器以消除任何和所有不必要的字符串复制和/或中间数据。看看你是否能胜过正则表达式版本。
关键是。正则表达式版本更易于理解和维护,而如果正确编写,您的手动词法分析器可能会更快。传统智慧说,帮自己一个忙,更喜欢前者。就Big-O复杂性而言,两者之间应该没有任何区别。它们是同一件事的两种形式。