多正则表达式匹配 (Multiple Regular Expression Matching) 中的动态 DFA 算法

前一段时间,在将 多正则表达式匹配工具 用于数十万任意的正则表达式时,之前一直担忧的问题终于出现了:NFA 转化 DFA 时的指数爆炸,那样的 DFA 根本建立不出来,由于那些正则表达式之间有不可预料的各类交集!正则表达式

这个问题对我打击很大,我甚至顿时以为 多正则表达式匹配工具 彻底是个废柴,最多,是个玩具!可是,只有挑战,才能激励人的斗志,挖掘人的潜能。我想起了曾经对之不屑一顾的动态 DFA 匹配算法,以前我在研究 RE2 时知道,RE2 在动态 DFA 的内存用量达到限定时,会抛弃已经建立整个动态 DFA,由于 DFA 的状态图比较复杂,节点之间互相引用,没法象普通的 Cache 同样部分的进行 Swap ,直觉上对它就没有好印象。算法

可是碰到组合爆炸这个魔咒,只有尝试一下,就先用 RE2 中的 set 试一下,先从那数十万正则表达式中任选一万个,re2 执行却是很快,但立马就提示 DFA Out Of Memory 了,而后我尝试将 re2 的内存设到 8G,仍是不行,浪费若干脑细胞,最后才惊奇地发现,Re2 的内存限制用的是 int 来表示的,而 int 的事实标准是32 bit,最大只能是 2G-1!设成 2G-1以后,re2 运行正常,性能还不错。工具

因而我开始加码,将全部正则表达式都扔进去,果真,re2 崩了!性能

虽然如此,可是至少代表,动态 DFA 解决该问题是可行的,我开始实现个人计划。一切都很顺利:测试

以前我实现其余算法时(r1303),扩展了自动机的字符集,而没有在自动机的状态上打专用 Tag,这个决定真是明智之举,在这里又用上了:google

ch=256 表示括号捕获 用于我最先实现 One Pass 正则表达式的 DFA 算法
ch=257 表示全匹配 以前在一遍扫描中捕获全部括号时,发现因括号位置太靠前而致使严重的性能问题,
就实现了两遍扫描捕获括号,第一遍只看全匹配,得到正则表达式ID,
第二遍专门针对具体正则ID捕获括号,性能要好的多
ch=258 表示伪Epsilon转移 这是实现动态 DFA 匹配的一个根基,不过这个伪 Epsilon 转移只起到一个链表的做用,将全部子 DFA 的根(初始状态)链起来


若是一切都照抄 re2 的实现,那不过是无聊的体力劳动。个人动态 DFA 实现,彻底创建在我以前的各类自动机算法之上,有了这个根基,个人实现相比 re2,有不少优点:spa

  1. 系统:如同一套数学理论,有定义、公理、定理,任何新的推论,都创建在以前已经确立的坚实的基础之上
  2. 简单:由于已经有了坚实的基础,并不须要引入多少全新的东西,就能解决新的问题
  3. 可靠:不须要验证已有的代码,只要证实新加入的代码是正确的,整个系统就是正确可靠的
  4. 高效:已有的代码已足够高效,只要新的代码没有冗余计算,最终的算法必然是高效的
事实证实了这些:专门用于实现动态 DFA 的代码也就 500 行左右,包含了算法的全部细节:
  1. 一个基于已有 DenseDFA 的 DynDFA 基础类
  2. 用于 subset 查找的专用 hash table 实现,包括 find, insert, hash resize, hash function, hash equal, hash cache, ...
  3. 动态 DFA 的 warm up ,将靠近完整 DFA 的根(初始状态) 附近的状态构造出来做为热点,re2 中则彻底没有考虑 warm up
  4. 动态 DFA 内存超限时,只抛弃不在 warm up 集合的那些状态,而不像 re2 同样抛弃整个动态 DFA
  5. 动态 DFA 的基础 NFA 只是简单地用 ch=258 将全部子 DFA 并到一块儿,并且每一个子 DFA 都是最小化的,同时全部的子 DFA 共享了相同的尾
  6. 动态 DFA 直接引用做为捕获括号(submatch) 的子 DFA
最后的测试代表,个人算法比 RE2 要快 4~7 倍,在达到这个性能的同时,内存用量还要小 5~8 倍,而且,对于让 re2 崩掉的那几十万个正则表达式,运行彻底正常,同时,还能捕获括号(submatch)。

使用:在  多正则表达式匹配工具 中,命令行参数 -D 表示生成的自动机匹配时用动态 DFA。
相关文章
相关标签/搜索