24

我注意到我的 Ruby (1.9) 脚本中有一些极端的延迟,经过一番挖掘,它归结为正则表达式匹配。我在 Perl 和 Ruby 中使用以下测试脚本:

珀尔:

$fname = shift(@ARGV);
open(FILE, "<$fname" );
while (<FILE>) {
    if ( /(.*?) \|.*?SENDING REQUEST.*?TID=(.*?),/ ) {
        print "$1: $2\n";
    }
}

红宝石:

f = File.open( ARGV.shift )
while ( line = f.gets )
    if /(.*?) \|.*?SENDING REQUEST.*?TID=(.*?),/.match(line)
        puts "#{$1}: #{$2}"
    end
end

我对两个脚本使用相同的输入,一个只有 44290 行的文件。每一项的时间是:

珀尔:

xenofon@cpm:~/bin/local/project$ time ./try.pl input >/dev/null

real    0m0.049s
user    0m0.040s
sys     0m0.000s

红宝石:

xenofon@cpm:~/bin/local/project$ time ./try.rb input >/dev/null

real    1m5.106s
user    1m4.910s
sys     0m0.010s

我想我在做一些非常愚蠢的事情,有什么建议吗?

谢谢

4

6 回答 6

7
regex = Regexp.new(/(.*?) \|.*?SENDING REQUEST.*?TID=(.*?),/)

f = File.open( ARGV.shift ).each do |line|
    if regex .match(line)
        puts "#{$1}: #{$2}"
    end
end

或者

regex = Regexp.new(/(.*?) \|.*?SENDING REQUEST.*?TID=(.*?),/)

f = File.open( ARGV.shift )
f.each_line do |line|
  if regex.match(line)
    puts "#{$1}: #{$2}"
  end
于 2012-04-20T09:28:39.760 回答
5

一种可能的差异是正在执行的回溯量。Perl 在回溯时可能会更好地修剪搜索树(即注意何时可能无法匹配模式的一部分)。它的正则表达式引擎是高度优化的。

首先,添加前导 « ^» 可能会产生巨大的影响。如果模式从位置 0 开始不匹配,那么它也不会在开始位置 1 匹配!所以不要试图在位置 1 匹配。

同样,« .*?» 并不像您想象的那样具有限制性,并且将它的每个实例替换为更具限制性的模式可以防止大量回溯。

你为什么不试试:

/
    ^
    (.*?)                       [ ]\|
    (?:(?!SENDING[ ]REQUEST).)* SENDING[ ]REQUEST
    (?:(?!TID=).)*              TID=
    ([^,]*)                     ,
/x

(不确定用« »替换第一个« »是否安全.*?[^|]所以我没有。)

(至少对于匹配单个字符串的模式来说,(?:(?!PAT).)是 to PATas [^CHAR]is to CHAR。)

如果允许« » 匹配换行符,使用/s可能会加快速度.,但我认为它很小。

在 Ruby 中使用 « » 而不是 « » 来匹配下面的空格可能会稍微快一些。(在最近的 Perl 版本中它们是相同的。)我使用后者是因为它更具可读性。\space[space]/x

于 2012-04-20T18:57:56.937 回答
5

来自perlretut 章节:在 Perl 部分中使用正则表达式- “搜索和替换”

(即使正则表达式出现在循环中,Perl 也足够聪明,只编译一次。)

我不太了解 Ruby,但我怀疑它确实会在每个循环中编译正则表达式。
(尝试从 LaGrandMere 的答案中验证它的代码)。

于 2012-04-20T09:48:29.610 回答
2

尝试使用(?>re)扩展。有关详细信息,请参阅Ruby 文档,此处引用:

这种结构 [..] 禁止回溯,这可以提高性能。例如,/a.*b.*a/当匹配一个包含 ana 后跟多个bs 但没有尾随 s的字符串时,该模式需要指数时间a。但是,这可以通过使用嵌套的正则表达式来避免 /a(?>.*b).*a/

File.open(ARGV.shift) do |f|
  while line = f.gets
    if /(.*?)(?> \|.*?SENDING REQUEST.*?TID=)(.*?),/.match(line)
      puts "#{$1}: #{$2}"
    end
  end
end
于 2013-04-10T14:18:22.577 回答
1

红宝石:

File.open(ARGV.shift).each do |line|
    if line =~ /(.*?) \|.*?SENDING REQUEST.*?TID=(.*?),/
        puts "#{$1}: #{$2}"
    end
end

match将方法更改为=~操作员。它更快,因为:

(Ruby 有 Benchmark。我不知道你的文件内容,所以我随机输入了一些内容)

require 'benchmark'

def bm(n)
    Benchmark.bm do |x|
    x.report{n.times{"asdfajdfaklsdjfklajdklfj".match(/fa/)}}
    x.report{n.times{"asdfajdfaklsdjfklajdklfj" =~ /fa/}}
    x.report{n.times{/fa/.match("asdfajdfaklsdjfklajdklfj")}}
    end
end

bm(100000)

输出报告:

       user     system      total        real
   0.141000   0.000000   0.141000 (  0.140564)
   0.047000   0.000000   0.047000 (  0.046855)
   0.125000   0.000000   0.125000 (  0.124945)

中间的一个正在使用=~. 它需要不到其他人的1/3。其他两个正在使用match方法。所以,=~在你的代码中使用。

于 2012-04-20T19:33:39.530 回答
1

与其他形式的匹配相比,正则表达式匹配比较耗时。由于您希望匹配行中间有一个长的静态字符串,因此请尝试使用相对便宜的字符串操作过滤掉不包含该字符串的行。这应该会减少需要通过正则表达式解析的工作(当然,这取决于您的输入内容)。

f = File.open( ARGV.shift )
my_re = Regexp.new(/(.*?) \|.*?SENDING REQUEST.*?TID=(.*?),/)
while ( line = f.gets )
    continue if line.index('SENDING REQUEST') == nil
    if my_re.match(line)
        puts "#{$1}: #{$2}"
    end
end
f.close()

因为我没有您的输入数据,所以我没有对这个特定版本进行基准测试。不过,我过去曾成功地做这样的事情,特别是对于冗长的日志文件,其中预过滤可以消除绝大多数输入而无需运行任何正则表达式。

于 2012-04-20T20:19:00.650 回答