检查字符串是否与 Ruby 中的 regexp 匹配的最快方法?

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

我的问题是,我必须“ egrep”通过一个庞大的字符串列表来查找哪些字符串与运行时给定的 regexp 匹配。我希望这个假设可以用来减少我的代码花在匹配 regexp 上的时间。

我加载 regexp

pattern = Regexp.new(ptx).freeze

我发现 string =~ patternstring.match(pattern)稍微快一点。

是否有其他技巧或快捷方式可以用来使这个测试更快?

124693 次浏览

根据正则表达式的复杂程度,您可以只使用简单的字符串切片。我不确定这个应用程序的实用性,也不确定它是否真的能提高速度。

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

This is a simple benchmark:

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.4.0版本中,re.match?(str)获胜(如@wiktor-stribi ew 所建议的) ,在以前的版本中,re =~ str似乎是最快的,尽管 str =~ re几乎和 re.match?(str)一样快。

#!/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)

Results 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)

re === str(案例比较)怎么样?

由于它的计算结果为 true 或 false,并且不需要存储匹配项、返回匹配索引等等,我想知道它是否是比 =~更快的匹配方法。


我测试过了。即使您有多个捕获组,=~仍然更快,但是它比其他选项更快。

BTW, what good is freeze? I couldn't measure any performance boost from it.

我想知道的是,是否有什么奇怪的方法可以使这个检查更快,也许可以利用 Regexp 中的一些奇怪的方法或一些奇怪的构造。

Regexp 引擎在实现搜索的方式上有所不同,但是一般来说,为了提高速度,它们会固定模式,并避免贪婪的匹配,特别是在搜索长字符串时。

在你熟悉某个特定引擎的工作原理之前,最好的做法是做基准测试,添加/删除锚点,尝试限制搜索,使用通配符或显式匹配,等等。

水果味 gem 对于快速基准测试非常有用,因为它非常聪明。Ruby 内置的 Benchmark代码也很有用,尽管你可以编写一些测试来欺骗你不小心。

我在 Stack Overflow 的许多答案中都使用了这两种方法,所以你可以通过搜索我的答案,看到许多小技巧和结果,从而了解如何编写更快的代码。

需要记住的最重要的一点是,在您知道哪里出现了减速之前过早地优化您的代码是不好的。

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

pattern.match?(string)

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

Regexp # 匹配?
增加了 Regexp#match?,它在不创建反向引用对象和更改 $~以减少对象分配的情况下执行 regexp 匹配。

为了完成 维克多 · 斯特里比真恶心Dougui的答案,我会说 /regex/.match?("string")大约和 "string".match?(/regex/)一样快。

Ruby2.4.0(1000000 ~ 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>

Ruby2.6.2(10000000 ~ 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/)会更快,这种差异可能只是由于机器的活动造成的。