PostgreSQL:一些正则表达式性能黑客

2021-02-19 17:43:15

正如我在添加src / test / modules / test_regex测试代码时提到的那样,我一直在愚弄我们的正则表达式引擎的一些性能改进。这是这项工作的最初成果。这主要与减少处理琐碎无限制模式(如"。*")的开销有关。

0001创建了"彩虹"的概念。正则表达式NFA中的弧线。您可以在“颜色和颜色映射”中阅读有关此背景的信息。 regex / README的一部分,但现在的基本要点是,代表NFA中的点("。&#34 ;,匹配任何东西)时,每种" color"都需要一个单独的弧正则表达式需要的(字符等效类)。这会花费大量的存储和处理工作量,尤其是在较大的正则表达式中,这些正则表达式往往具有很多颜色。我们可以替换这样的"彩虹"带有标记有特殊颜色RAINBOW的单个弧的弧。这样做值得,因为它可以节省空间和时间。例如,在test_regex.sql(中等大小的实际RE)中的reg-33.15.1测试用例上,我发现HEAD需要1377614字节来表示已编译的RE,并且pg_regcomp()期间的峰值空间使用量是3124376个字节。使用此补丁程序,RE的大小降至1077166字节(节省21%),而峰值编译空间为2800752字节(节省10%)。此外,该测试用例的运行时间从〜57ms降至〜44ms,节省了22%。 (这主要是衡量RE的编译时间。执行时间也应该减少一点,因为miss()需要考虑较少的弧;但是节省的是冷代码路径,因此无关紧要。) ; t当然是惊人的数字,但是对于所需的代码量来说,这似乎很值得。

一个可能的争论点是,我在regexport.h API中公开了彩虹弧的概念,这将迫使该API的使用者适应---参见对contrib / pg_trgm所做的更改。我对此不太担心,因为我怀疑pg_trgm是该API在任何地方的唯一使用者。 (无论如何,codesearch.debian.net都一无所知。)我们原则上可以通过让regexport函数将每种颜色的彩虹弧扩展为一个彩虹弧来隐藏更改,但这似乎很简单。 pg_trgm肯定不会将其视为改进,并且一般而言,该API的用户应该喜欢识别虹彩,因为他们可能能够应用依赖于此的优化。

这使我们达到了0002,这正是这种优化。这里的想法是,当匹配类似于"。"的子NFA时,将逐字符扫描短路。或"。*"或它的变体,即它将匹配具有一定数量字符的任何序列。这需要能够识别特定的NFA状态对由一条彩虹链接的能力,因此,当明确表示彩虹时,要做的工作就不那么麻烦了。使我对此感兴趣的示例改编自Tcl故障报告:

在我的机器上,这在HEAD上大约需要6秒钟,因为有O(N ^ 2)效果:我们尝试为第一个"(。*)"匹配sub-NFA。捕获组到每个可能的起始字符串,并且只有在昂贵地验证了重言式匹配之后,我们才检查下一个字符是否为" ="。通过不必执行每个字符的工作来确定。*与子字符串匹配,可以消除O(N ^ 2)行为,并将时间降至大约7毫秒。

(还可以想象通过在验证捕获组匹配之前重新安排内容以检查" =匹配来解决此问题。我希望以后可以考虑这一想法,因为它可以对于可变部分不仅仅为"。*"的情况提供帮助,但是我对如何做到这一点并没有明确的想法,无论如何还是"。*&#34 ;足够普遍,以至于当前的更改仍然会有所帮助。)

0002补丁有两个非样板部分。一个是checkmatchall()函数,该函数确定NFA是否为全部匹配,如果是,则为最小和最大匹配长度。一旦您了解了regex引擎在" pre"上的功能,实际上这并不是很复杂。和" post"状态。 (有关正则表达式的一些信息,请参阅regex / README的“详细语义”部分。我试图在补丁程序中加以澄清。)除了这些端点条件外,它只是递归图搜索。另一个困难的部分是rege_dfa.c中的更改,以在运行时提供实际的短路行为。之所以如此,是因为它试图模仿一些过于复杂和文档不足的代码,尤其是在longest()和shortest()中。我认为这是对的;我已经研究并测试了它。但是它可能会使用更多的眼球。

值得注意的是,我不得不在test_regex.sql中添加更多测试用例,以正确行使matchuntil()的短路部分。那只是用于后向约束,所以我们不会碰到短路路径,除非有类似'(?< = ..)'之类的东西。愚蠢的。

为了测试补丁的正确性,我认为使用一些真实的正则表达式会很不错,同样重要的是,将真实的正则表达式应用于这些真实的文本字符串。

因此,我修补了Chromium的v8正则表达式引擎,以记录访问网站时已编译的实际正则表达式,并在运行正则表达式时在运行时将作为正则表达式的文本字符串应用于该正则表达式。

我将正则表达式和文本字符串作为base64编码的字符串记录到STDOUT中,以使其易于grep输出数据,因此可以将其导入PostgreSQL进行分析。

总共,我刮擦了大约5万个网站的首页,该网站产生了4500万个要导入的测试行,当GROUP BY模式和标志减少到235k个不同的正则表达式模式和150万个不同的文本字符串主题时,这些行就会被导入。

SELECT *,SUM(COUNT)OVER()FROM(SELECT SELECT,COUNT(*)FROM pattern GROUP BY标志)AS x ORDER BY COUNT DESC;标志|计数|总和------- + -------- + -------- | 150097 | 235204我| 43537 | 235204克| 22029 | 235204 gi | 15416 | 235204克| 2411 | 235204 gim | 602 | 235204 m | 548 | 235204 im | 230 | 235204年| 193 | 235204 gy | 60 | 235204 giy | 29 | 235204朱| 26 | 235204 u | 11 | 235204 iy | 6 | 235204股| 5 | 235204 gimu | 2 | 235204 iu | 1 | 235204我的| 1 | 235204(18行)

如我们所见,最没有标志是最常见的,其后是" i"。旗帜。

PostgreSQL可以理解大多数Javascript-regexes(97%),只有3%会产生某种错误,这并不意外,因为某些\ w和\ W之类的Javascript-regex功能在PostgreSQL中具有不同的语法:

SELECT *,SUM(COUNT)OVER()FROM(SELECT is_match,error,COUNT(*)FROM subject GROUP BY is_match,error)AS x ORDER BY count DESC; is_match |错误计数|总和---------- + -------------------------------------- ------------------------- + -------- + --------- f | | 973987 | 1489489吨| | 474225 | 1489489 |无效的正则表达式:无效的转义\序列| 39141 | 1489489 |无效的正则表达式:无效的字符范围| 898 | 1489489 |正则表达式无效:反向引用编号无效| 816 | 1489489 |无效的正则表达式:方括号[]不平衡| 327 | 1489489 |无效的正则表达式:无效的重复计数| 76 | 1489489 |无效的正则表达式:量词操作数无效| 17 | 1489489 |无效的正则表达式:括号()不平衡| 1 | 1489489 |无效的正则表达式:正则表达式太复杂| 1 | 1489489(10行)

查看统计信息很有趣之后,我们继续来看一下HEAD(8063d0f6f56e53edd991f53aadc8cb7f8d3fdd8f)和应用了这两个补丁之间是否存在可观察到的差异。

为了检测任何差异,对于每对(正则表达式模式,文本字符串主题)对,由运行HEAD的PL / pgSQL函数设置列,is_match布尔捕获的text []错误文本:

开始_is_match:= _subject〜_pattern; _captured:= regexp_match(_subject,_pattern);当其他人随后更新主题时设置异常= SQLERRM WHERE subject_id = _subject_id;继续;结尾;更新主题SET is_match = _is_match,捕获= _捕获WHERE subject_id = _subject_id;

SELECT is_match<> (主题〜模式)AS is_match_diff,从regexp_match(主题,模式)捕获到IS DISTINCT与AS捕获_diff,COUNT(*)在错误为NULL并且(is_match<>(主题〜模式)的主题的情况下从COUNTexp_match(主题,模式))GROUP BY 1,2 ORDER BY 3 DESC;

该查询首先在未修补的HEAD上运行以验证其未检测到结果。确实有0行,花了很长时间才能完成查询:

Warning: Can only detect less than 5000 characters