介绍
“不要使用正则表达式,否则您将遇到2个问题,而不是1“” - 这就是专家所说的。对于那些想有效地搜索大量模板的顽皮的人来说,还有什么呢?
当然,对于此特定问题,有一些很酷的解决方案,例如 ragel 或 re2c 。但是,对于我的项目而言,掌握这些优秀技术的范围似乎是不切实际的。
在本文中,我们将查看GO中标准Regexp库的替代方案,并基于它们的速度和内存消耗。我们还将从实际的角度考虑它们之间的差异。
类似物
目前,我找到了可用于在GO中查找模式的默认regexp 的以下工作替代方案(在括号中给出了基准中使用的版本): P>
- 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 使用):
基准#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 的结果时,我缺乏以下问题的答案:
- 上面库处理大文件的速度?
- 分组正则表达式时处理的速度更快?
- 未经文本中没有匹配的正则表达式将多快?
- 不同的库使用了多少内存?
- 我可以使用分组来编译多少个正格?
基准测试器差异
要回答这些问题,我编写了一个小型基准程序,该程序可用于在速度和内存使用方面比较不同的正则发动机。如果您想自己测试或评估使用方法的正确性,则是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 em> td>
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文件时,不同解决方案消耗多少内存。下面,我为每个个人发言权提供了结果,并且消耗的内存总量:
lib | 电子邮件,MB | 电话,MB | uri,mb | ssn,mb | 比特币,MB | 不匹配,kb | 总计,GB总组,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 td> | ||||
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中使用这些“发现”:)。