在Ruby中,检查字符串是否与正则表达式匹配的最快方法是什么?

122

在Ruby中,最快的检查字符串是否匹配正则表达式的方法是什么?

我的问题是我需要在一个大量的字符串列表中搜索匹配给定运行时正则表达式的字符串。 我只关心字符串是否与正则表达式匹配,而不关心它匹配的位置或匹配组的内容。 希望这个假设可以减少代码匹配正则表达式所需的时间。

我使用以下代码加载正则表达式:

pattern = Regexp.new(ptx).freeze

我发现string =~ patternstring.match(pattern)略快。

还有其他什么技巧或快捷方式可以使这个测试变得更快吗?


如果您不关心匹配组的内容,为什么要使用它们?将它们转换为非捕获组可以使正则表达式更快。 - Mark Thomas
1
由于正则表达式是在运行时提供的,我假设它是无限制的,在这种情况下,正则表达式内部可能存在对分组的引用,因此通过修改正则表达式将它们转换为非捕获形式可能会修改结果(除非您还检查内部引用,但问题变得越来越复杂)。我觉得使用“=~”比string.match更快,这让我感到好奇。 - djconnel
在这里冻结正则表达式的好处是什么? - Hardik
7个回答

146

从Ruby 2.4.0开始,您可以使用RegExp#match?

pattern.match?(string)

Regexp#match?Ruby 2.4.0发布说明中明确列为性能增强功能,因为它避免了其他方法(如Regexp#match=~)执行的对象分配:

Regexp#match?
添加了Regexp#match?,它执行正则表达式匹配而不创建后向引用对象,并更改$~以减少对象分配。


6
谢谢您的建议。我已经更新了基准测试脚本,Regexp#match? 的速度确实比其他替代方案快了至少50%。 - gioele
这里是 Regexp.match? 的 Ruby 文档链接:https://ruby-doc.org/3.2.0/Regexp.html#method-i-match-3F - stwr667

81

这是一个简单的基准测试:

require 'benchmark'

"test123" =~ /1/
=> 4
Benchmark.measure{ 1000000.times { "test123" =~ /1/ } }
=>   0.610000   0.000000   0.610000 (  0.578133)

"test123"[/1/]
=> "1"
Benchmark.measure{ 1000000.times { "test123"[/1/] } }
=>   0.718000   0.000000   0.718000 (  0.750010)

irb(main):019:0> "test123".match(/1/)
=> #<MatchData "1">
Benchmark.measure{ 1000000.times { "test123".match(/1/) } }
=>   1.703000   0.000000   1.703000 (  1.578146)

如果你只是想检查文本是否包含正则表达式,那么使用=~更快。但它返回的值取决于你想要什么。


2
正如我所写的那样,我已经发现=~match更快,当操作更大的正则表达式时,性能增长不会太明显。我想知道的是,是否有任何奇怪的方法可以使这个检查变得更快,也许是利用Regexp中的一些奇怪的方法或一些奇怪的构造。 - gioele
我认为没有其他解决方案。 - Dougui
!("test123" !~ /1/)是什么意思? - ma11hew28
1
@MattDiPasquale,两倍逆操作速度不应该比“test123”匹配/1/正则表达式的速度更快。 - Dougui
1
如果只是检查文本是否包含正则表达式,那么/1/.match?("test123")"test123" =~ /1/更快。 - noraj

47
这是我在网上找到一些文章后运行的基准测试。
在2.4.0中,赢家是re.match?(str)(由@wiktor-stribiżew建议),在之前的版本中,re =~ str似乎是最快的,虽然str =~ re几乎和它一样快。
#!/usr/bin/env ruby
require 'benchmark'

str = "aacaabc"
re = Regexp.new('a+b').freeze

N = 4_000_000

Benchmark.bm do |b|
    b.report("str.match re\t") { N.times { str.match re } }
    b.report("str =~ re\t")    { N.times { str =~ re } }
    b.report("str[re]  \t")    { N.times { str[re] } }
    b.report("re =~ str\t")    { N.times { re =~ str } }
    b.report("re.match str\t") { N.times { re.match str } }
    if re.respond_to?(:match?)
        b.report("re.match? str\t") { N.times { re.match? str } }
    end
end

结果 MRI 1.9.3-o551:

$ ./bench-re.rb  | sort -t $'\t' -k 2
       user     system      total        real
re =~ str         2.390000   0.000000   2.390000 (  2.397331)
str =~ re         2.450000   0.000000   2.450000 (  2.446893)
str[re]           2.940000   0.010000   2.950000 (  2.941666)
re.match str      3.620000   0.000000   3.620000 (  3.619922)
str.match re      4.180000   0.000000   4.180000 (  4.180083)

结果 MRI 2.1.5:

$ ./bench-re.rb  | sort -t $'\t' -k 2
       user     system      total        real
re =~ str         1.150000   0.000000   1.150000 (  1.144880)
str =~ re         1.160000   0.000000   1.160000 (  1.150691)
str[re]           1.330000   0.000000   1.330000 (  1.337064)
re.match str      2.250000   0.000000   2.250000 (  2.255142)
str.match re      2.270000   0.000000   2.270000 (  2.270948)

MRI 2.3.3的结果(似乎有一个正则表达式匹配的回归):

$ ./bench-re.rb  | sort -t $'\t' -k 2
       user     system      total        real
re =~ str         3.540000   0.000000   3.540000 (  3.535881)
str =~ re         3.560000   0.000000   3.560000 (  3.560657)
str[re]           4.300000   0.000000   4.300000 (  4.299403)
re.match str      5.210000   0.010000   5.220000 (  5.213041)
str.match re      6.000000   0.000000   6.000000 (  6.000465)

MRI 2.4.0 结果:

$ ./bench-re.rb  | sort -t $'\t' -k 2
       user     system      total        real
re.match? str     0.690000   0.010000   0.700000 (  0.682934)
re =~ str         1.040000   0.000000   1.040000 (  1.035863)
str =~ re         1.040000   0.000000   1.040000 (  1.042963)
str[re]           1.340000   0.000000   1.340000 (  1.339704)
re.match str      2.040000   0.000000   2.040000 (  2.046464)
str.match re      2.180000   0.000000   2.180000 (  2.174691)

只是补充一下,字面形式比这些更快。例如,/a+b/ =~ strstr =~ /a+b/。即使在通过函数迭代它们时也是有效的,我认为这足够好,可以考虑比将正则表达式存储和冻结在变量上更好。我使用ruby 1.9.3p547、ruby 2.0.0p481和ruby 2.1.4p265测试了我的脚本。这些改进可能是在以后的补丁中进行的,但我还没有计划用早期版本/补丁进行测试。 - konsolebox
我以为 !(re !~ str) 可能会更快,但实际上并不是。 - ma11hew28

7

对于re === str(大小写比较),怎么处理呢?

既然它只是评估为true或false,没有必要存储匹配项,返回匹配索引等内容,那么我想它是否是比使用=~更快的一种匹配方式。


好的,我测试了一下。即使有多个捕获组,=~仍然比re === str更快,但是它比其他选项都要快。

顺便问一下,freeze有什么用?我无法从中测量到任何性能提升。


freeze 的效果不会在结果中显示,因为它发生在基准循环之前,并且作用于模式本身。 - the Tin Man

5

根据您的正则表达式的复杂程度,您可能只需要使用简单的字符串切片。我不确定这对您的应用程序是否实用,或者它是否会真正提供任何速度改进。

'testsentence'['stsen']
=> 'stsen' # evaluates to true
'testsentence'['koala']
=> nil # evaluates to false

我无法使用字符串切片,因为正则表达式是在运行时提供的,我对此没有任何控制。 - gioele
你可以使用字符串切片,但不能使用固定字符串进行切片。使用变量代替引号中的字符串,它仍然可以工作。 - the Tin Man

3
我在想是否有任何奇怪的方式可以使这个检查过程更快,也许可以利用Regexp中的某些奇怪方法或者一些奇怪的结构。
不同的Regexp引擎在实现搜索时有所不同,但通常情况下,将模式锚定以提高速度,并避免贪婪匹配,特别是在搜索长字符串时。
最好的方法是,在熟悉特定引擎的工作原理之前,进行基准测试并添加/删除锚点,尝试限制搜索,使用通配符与显式匹配等。 Fruity宝石非常有用,因为它很聪明,可以快速进行基准测试。Ruby内置的Benchmark代码也很有用,不过你可以编写测试来欺骗自己,所以要小心。
我在Stack Overflow上的许多回答都使用了这两种方法,您可以搜索我的回答,会发现很多小技巧和结果,可以给您提供写更快代码的想法。
最重要的是,要记住,在知道哪里出现了问题之前,过早地优化代码是不好的。

1
为了补充Wiktor StribiżewDougui的回答,我想说/regex/.match?("string")"string".match?(/regex/)的速度几乎相同。
Ruby 2.4.0 (10,000,000约2秒)
2.4.0 > require 'benchmark'
 => true 
2.4.0 > Benchmark.measure{ 10000000.times { /^CVE-[0-9]{4}-[0-9]{4,}$/.match?("CVE-2018-1589") } }
 => #<Benchmark::Tms:0x005563da1b1c80 @label="", @real=2.2060338060000504, @cstime=0.0, @cutime=0.0, @stime=0.04000000000000001, @utime=2.17, @total=2.21> 
2.4.0 > Benchmark.measure{ 10000000.times { "CVE-2018-1589".match?(/^CVE-[0-9]{4}-[0-9]{4,}$/) } }
 => #<Benchmark::Tms:0x005563da139eb0 @label="", @real=2.260814556000696, @cstime=0.0, @cutime=0.0, @stime=0.010000000000000009, @utime=2.2500000000000004, @total=2.2600000000000007> 

Ruby 2.6.2(100,000,000个 ~20秒)

irb(main):001:0> require 'benchmark'
=> true
irb(main):005:0> Benchmark.measure{ 100000000.times { /^CVE-[0-9]{4}-[0-9]{4,}$/.match?("CVE-2018-1589") } }
=> #<Benchmark::Tms:0x0000562bc83e3768 @label="", @real=24.60139879199778, @cstime=0.0, @cutime=0.0, @stime=0.010000999999999996, @utime=24.565644999999996, @total=24.575645999999995>
irb(main):004:0> Benchmark.measure{ 100000000.times { "CVE-2018-1589".match?(/^CVE-[0-9]{4}-[0-9]{4,}$/) } }
=> #<Benchmark::Tms:0x0000562bc846aee8 @label="", @real=24.634255946999474, @cstime=0.0, @cutime=0.0, @stime=0.010046, @utime=24.598276, @total=24.608321999999998>

注意:时间可能会有所不同,有时/regex/.match?("string")更快,有时"string".match?(/regex/),这些差异可能仅由于机器活动引起。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接