最好的延期替代方案。基准。情节。
#编程 #go #regex #learning

介绍

“不要使用正则表达式,否则您将遇到2个问题,而不是1“” - 这就是专家所说的。对于那些想有效地搜索大量模板的顽皮的人来说,还有什么呢?

当然,对于此特定问题,有一些很酷的解决方案,例如 ragel re2c 。但是,对于我的项目而言,掌握这些优秀技术的范围似乎是不切实际的。

在本文中,我们将查看GO中标准Regexp库的替代方案,并基于它们的速度和内存消耗。我们还将从实际的角度考虑它们之间的差异。

类似物

目前,我找到了可用于在GO中查找模式的默认regexp 的以下工作替代方案(在括号中给出了基准中使用的版本):

  • go-re2(1.3.0) - 尽可能简单地替换默认的REGEXP。使用 c ++ re2 在处理大型输入或复杂表达式时提高性能;
  • regexp2(1.10.0) - go 的功能丰富的Regexp引擎。它没有像内置Regexp软件包那样保证运行时,但与Perl5和.net兼容;
  • go-pcre(1.0.0) - 使用 libpcre libpcre ++ 提供对 perl 兼容正则表达式的支持。可以使用JIT汇编,使该叉子比其对应物快得多。不利的一面是,您需要 libpcre3-dev 依赖关系;
  • rure-go(REGEX 1.9.3) - 使用 Rust rust cgo 绑定。缺点是需要编译的 rust 库依赖性;
  • gohs(1.2.2 + HS5.4.1) - 专为高性能而设计的正则发动机。它被实现为提供简单 c-api 的库。它还需要第三方依赖性的汇编和联系;
  • go-yara-一种用于识别和分类恶意软件样本的工具。尽管 yara 具有模板和正则表达式的功能,但它非常有限,因此我不会将此库包括在即将到来的测试中。

现有基准

在我们开始比较上述解决方案之前,值得与 go 中的标准Regex库一起展示多么糟糕的事情。我找到了project,在其中作者比较了各种语言的标准正则发动机的性能。该基准的重点是在预定义的文本上反复运行3个正则表达式。 go 第三名在此基准标准中! 从头开始...

语言 电子邮件(MS) uri(ms) ip(ms) 总(MS)
Nim Regex 1.32 26.92 7.84 36.09
Nim 22.70 21.49 6.75 50.94
Rust 26.66 25.70 5.28 57.63
... ... ... ... ...
python pypy3 258.78 221.89 257.35 738.03
python 3 273.86 190.79 319.13 783.78
GO 248.14 241.28 360.90 850.32
C ++ STL 433.09 344.74 245.66 1023.49
c#mono 2859.05 2431.87 145.82 5436.75
据我所知,

基准对正则发动机并不是最简单的主题,因为您需要了解实现和算法详细信息以进行正确的比较。从其他基准测试中,我可以突出显示以下内容:

  • Benchmarks Game-对每种语言的优化版本的发动机进行比较。例如, go 不再处于底部,但它离那里的理想程度仍然很远...当然,它不使用本地库,而是PCRE上的包装器 - go -pcre ,在类似物中指定。
  • Performance comparison of regular expression engines-比较不同的正则发动机(PCRE,PCRE-DFA,TRE,ONIGURUMA,RE2,PCRE-JIT)。
  • A comparison of regex engines-在这里,作者试图使用不同的正则表达式比较引擎,这可能会根据引擎实施而复杂化。在这个基准中,作者的前三引擎是:Hyperscan,PCRE(带有JIT汇编)和Rust Regex( rure 使用): [Source](https://rust-leipzig.github.io/regex/2017/03/28/comparison-of-regex-engines) More score is better

基准#1

现在,让我们尝试将类似物与其他语言的默认正则发动机库进行比较。还可以查看它们与默认 go Regex 的速度更快。为此,我通过添加新的库代码来更新上述项目。这是我在计算机上运行它后得到的:

语言 电子邮件(MS) uri(ms) ip(ms) 总(MS)
Go Rure 2.61 2.11 3.33 8.05
C ++ SRELL 1.74 3.03 10.67 15.45
Rust 11.66 1.70 5.13 18.48
Nim 13.98 14.35 3.13 31.46
php 14.43 14.63 4.87 33.93
GO PCRE 14.18 14.98 6.21 35.37
c#.net core 10.83 5.10 26.71 42.64
javascript 42.78 30.17 0.92 73.87
GO RE2 35.81 37.86 33.79 107.46
GO Hyperscan 90.17 31.64 8.68 130.49
perl 92.51 66.42 22.51 181.44
飞镖 73.48 63.60 80.52 217.59
c pcre2 110.87 100.57 10.50 221.94
kotlin 120.31 163.53 293.69 577.53
java 205.14 201.55 295.68 702.36
python 3 246.26 176.01 303.08 725.34
C ++ STL 328.87 273.43 230.35 832.64
GO 270.19 275.73 504.79 1050.71
GO REGEXP2 1703.51 1482.60 64.46 3250.57
c#mono 2543.82 2139.44 110.37 4793.64

ps。稍微缩短了桌子,因此请检查project fork以查看所有结果。
pss。在这种情况下,虚拟化可能会影响结果(测试是在Windows 11上的Docker环境中执行的)。

仍然,您可以看到某些库可以更快的速度!我们甚至在使用Rust库的GO库ðUrt库ð¥´ðð¼ââââ€。也许这是该解决方案的作者试图在他的repository中向我们解释的原因。

结果,几乎所有替代解决方案都使我们加快了 8-130 时!除了 regexp2 ,它比标准库慢。

基准#2

问题

在研究现有基准和基准#1 的结果时,我缺乏以下问题的答案:

  1. 上面库处理大文件的速度?
  2. 分组正则表达式时处理的速度更快?
  3. 未经文本中没有匹配的正则表达式将多快?
  4. 不同的库使用了多少内存?
  5. 我可以使用分组来编译多少个正格?

基准测试器差异

要回答这些问题,我编写了一个小型基准程序,该程序可用于在速度和内存使用方面比较不同的正则发动机。如果您想自己测试或评估使用方法的正确性,则是code

该基准的以下功能值得一提:

  • 在下面的测试中,我使用了 5 不同的正则表达式:
allRegexps["email"] = `(?P<name>[-\w\d\.]+?)(?:\s+at\s+|\s*@\s*|\s*(?:[\[\]@]){3}\s*)(?P<host>[-\w\d\.]*?)\s*(?:dot|\.|(?:[\[\]dot\.]){3,5})\s*(?P<domain>\w+)`
allRegexps["bitcoin"] = `\b([13][a-km-zA-HJ-NP-Z1-9]{25,34}|bc1[ac-hj-np-zAC-HJ-NP-Z02-9]{11,71})`
allRegexps["ssn"] = `\d{3}-\d{2}-\d{4}`
allRegexps["uri"] = `[\w]+://[^/\s?#]+[^\s?#]+(?:\?[^\s#]*)?(?:#[^\s]*)?`
allRegexps["tel"] = `\+\d{1,4}?[-.\s]?\(?\d{1,3}?\)?[-.\s]?\d{1,4}[-.\s]?\d{1,4}[-.\s]?\d{1,9}`

很好,我应该像其他基准作者一样使用棘手的正则表达式 - 检查算法的“弱点”。但是我对发动机的细节没有太多见解,因此我使用了通用的辩论率。这就是为什么我认为可以从实际角度评估库的不同参数。

  • 而不是静态文件,我们将使用包含匹配项的字符串,在内存中多次重复以模拟不同尺寸的文件:
var data = bytes.Repeat([]byte("123@mail.co nümbr=+71112223334 SSN:123-45-6789 http://1.1.1.1 3FZbgi29cpjq2GjdwV8eyHuJJnkLtktZc5 Й"), config.repeatScanTimes)
  • 除了在数据上顺序运行Regexps外,还将有一个单独的运行,并带有指定的正则表达式分组,它们将采用以下表格:
`(?P<name_1>regexp_1)|(?P<name_2>regexp_2)|...|(?P<name_N>regexp_N)`

顺便说一句,Hyperscan具有特殊的功能,我们可以在其中构建Regexps的数据库并使用它来针对数据。在基准测试中,我将使用此方法。

  • 基准#1 不同,对于每个Regexp,我将测量找到结果的时间,而无需考虑汇编时间;
  • 最终,我们将以以下形式获得每个库和每个REGEXP的结果:
Generate data...
Test data size: 100.00MB

Run RURE:
  [bitcoin] count=1000000, mem=16007.26KB, time=2.11075s 
  [ssn] count=1000000, mem=16007.26KB, time=62.074ms 
  [uri] count=1000000, mem=16007.26KB, time=69.186ms 
  [tel] count=1000000, mem=16007.26KB, time=83.101ms 
  [email] count=1000000, mem=16007.26KB, time=172.915ms 
Total. Counted: 5000000, Memory: 80.04MB, Duration: 2.498027s
...

1-2。处理大文件并分组正则表达式

通过“大文件”,是指从我们的预定义字符串中生成的数据量等于 100MB 。当然,这不是一个大文件,但是我不想等待几个小时的结果。

此外,尝试将所有正则表达式分组为一个,并查看其影响性能的程度是有道理的。假设是,在数据上进行的regexps的顺序运行应比单个运行式的REGEXP需要更长的时间。

好吧,让我们运行基准测试,看看每个库处理数据的速度。您可以使用我的工具进行以下操作:

# Bench all libs, repeat the line 1000000 times (100MB)
regexcmp 1000000 
# Bench individual lib
regexcmp 1000000 rure
# See all options
regexcmp -h

结果,我们有以下数据:

lib 电子邮件 tel uri ssn 比特币 总计 总组
rure 0.173S 0.083S 0.069S 0.062S 2.11S 2.49S 10.13S
pcre 49.5S 0.367S 2.92S 2.07S 2.19S 57.07S 9.94S
re2 0.954S 0.63S 0.956S 0.945S 1.05S 4.54S 3.24S
Hyperscan 0.469S 1.09S 0.669S 0.174S 1.97S 4.38S 4.97S
Regexp2 126.4S 1.02S 21.51S 2.63S 3.38S 154.9S 20.65S
GO Regexp 11.22S 1.52S 3.62S 2.66S 3.32S 22.36S 26.49S

下面的图显示了以顺序模式和使用分组:
的所有Regexps的100MB数据的处理时间

结论

  • 分组确实可以显着提高执行速度,但在某些情况下会使情况变得更糟:);
  • 顺序处理中最快的事实证明是 - rure ,分组 - re2 ;
  • 某些REGEXP可能会引起某些库的问题(是时候在 Regexp2 pcre 中找到email的时间);
  • 现在很难说某些解决方案比标准库快180倍,最大增益为 x8-9

3.不匹配的辩论权

在上一种情况下,我们模拟了数据中总是有匹配的理想情况。但是,如果文本中没有REGEXP的命中,该怎么办?

在此测试中,我还添加了不匹配数据的SSN的 5 修改的Regexps。在这种情况下,SSN表示\d{3}-\d{2}-\d{4} regexp和Non-matching -\d{3}-\d{2}-\d{4}1。没有什么大区别?但是,让我们看看它如何影响找到所有匹配的时间:

lib ssn 非匹配 总计 总组
rure 0.06S 0.083S 2.93S 15.68S
pcre 2.06S 4.21S 81.02S 13.52S
re2 0.960S 0.449S 6.75S 3.26S
Hyperscan 0.175S 0.043S 4.61S 4.94S
Regexp2 2.73S 3.09S 171.1S 15.72S
GO Regexp 2.66S 2.57S 35.13S 37.66S

下面的图显示了处理所有 10 regexps所花费的时间,由Non-matching处理时间排序:

结论

  • 这次是相同的:顺序处理中最快的是 - rure ,分组表达式 - re2 ;
  • pcre 再次有所不同, 2x 在顺序模式下处理non-matching regexps的时间;
  • 当没有匹配项时,某些算法要快得多( re2 hyperscan );

4.记忆消耗

现在,让我们看看在处理100MB文件时,不同解决方案消耗多少内存。下面,我为每个个人发言权提供了结果,并且消耗的内存总量:

总计,GB
lib 电子邮件,MB 电话,MB uri,mb ssn,mb 比特币,MB 不匹配,kb 总组,GB
rure 16 16 16 16 16 0.0001 0.08 0.08
pcre 186 186 186 186 186 0.38 0.93 0.9
re2 254 254 254 255 254 100473 1.77 0.97
Hyperscan 314 314 314 314 314 0.68 1.57 1.7
Regexp2 997 854 854 854 902 396006 6.44 5.23
GO REGEX 191 144 144 160 208 3.62 0.78 1.8

下图显示了库用来处理 10 regexps(如最后一个测试中)所用的内存,并按“非击击”时间进行排序:

结论:

  • rure 的实际记忆消耗使其令人惊讶;
  • regexp2 是饥饿的资源,比竞争对手的记忆力大得多;
  • re2 ,尽管速度速度,但并不是最记忆的解决方案;
  • GO REGEX 处于良好状态,在顺序模式下成本相对较低。

5.最大辩论额度数

似乎回答了主要问题。现在,让我们看看可以使用不同的解决方案来编译的最大沟通数量。在这种情况下,我们将采用单个式言论,并分组多次重复。
为此,我将使用URI Regexp:

go
[\ w]+:// [^/\ s?#]+[^\ s?#]+(?\?[^\ s#] )?(? ^\ s] )?

接下来,我已经列出了编译Regexps的结果以及它们使用的内存。第一行中的数字是组中的URI表达式的数量:

lib 100 250 500 1000 5000 10000
rure 0.38MB
pcre 0.40MB 2.37MB
re2 7.10MB 15.51MB 38.35MB 92.79MB 1181MB
Hyperscan 0.03MB 0.06MB 0.12MB 0.25MB 1.21MB 2.52MB
Regexp2 1.06MB 3.96MB 12.56MB 43.93MB 926MB 3604MB
GO REGEX 1.29MB 4.52MB 14.02MB 47.18MB 942MB 3636MB

结论

  • 正如我们所看到的,有些解决方案对编译的Regexps的大小有限制;
  • hyperscan 不仅允许使用大量的左右,而且还使用最小的内存来汇编的regexps;
  • regexp2 go regex 具有可比的内存消耗,还允许编译大量的左右;
  • re2 在编译时消耗最多的内存。

结论

我希望您能在 go go 中学习替代解决方案,并且根据我提供的数据,每个人都可以自己得出一些结论,这将允许您为您的情况选择最合适的正则解决方案。

我们可以比较这些库,它们使用的算法,很长一段时间的详细信息/最差方面。我只是想在大多数一般情况下显示它们之间的区别。

因此,我建议您查看rure-go的最大速度加速,但是如果您需要最简单的库安装而无需依赖,那就是go-re2。在处理大量Regexps hyperscan的情况下,将是一个不错的选择。另外,不要忘记在某些库中使用 cgo 的成本。

至于我,我一定会在未来的project中使用这些“发现”:)。