【问题标题】:optimizing regex substitution runtime优化正则表达式替换运行时
【发布时间】:2020-04-14 23:29:06
【问题描述】:

我有一些代码可以进行大量的正则表达式替换。从本质上讲,它归结为这个正则表达式,它在我的一个小示例测试用例中发生了大约 50K 次:

$string=~s/$pattern/$replacement/g ;

我已经通过 qr// 在所有模式中预编译模式(此测试用例中大约有 2.5K 模式)。我已经用 NYTProf 对此进行了分析,并看到正则表达式引擎的子例程使用的时间如下:

# spent  39.7s making 49461026 calls to CORE:regcomp, avg 802ns/call
# spent  7.94s making 49461026 calls to CORE:subst, avg 161ns/call
# spent  6.61s making 49461026 calls to CORE:match, avg 134ns/call

然而,根据分析器,这条线在约 50K 次调用中所用的时间约为 300 秒。所以这本质上意味着~53s被正则表达式引擎使用,而有~250s的开销?这个开销包括什么?我想字符串在修改后需要在内存中动态重新分配,但实际上正则表达式只匹配很少几次,所以我认为这不是开销所在。

我还能做些什么来减少这个运行时间?模式和替换都是简单的字符串,它们不使用正则表达式的功能(唯一真正使用的正则表达式字符是单词边界 - $pattern 开头和结尾的 \b,否则只是一系列固定的单词字符)

编辑: 在我在这里提出问题后,我实际上意识到了一个解决方案。让我澄清一下原始代码的样子,然后解释解决方案是否对将来有帮助。

简化原代码:

foreach my $string (@strings) {
  foreach my $pattern (@patterns) {
    my $replacement = $pattern2replacement{$pattern} ;
    my $compiled_pattern = $pattern2compiled{$pattern} ;
    $string=~s/$compiled_pattern/$replacement/g ;
    # do something with $string
  }
}

在实际代码中,内部 foreach 位于子例程中,在进入 foreach 之前正在进行其他检查/预处理。此外,外部 foreach 并不是一个真正的单一的,而是散布在代码中的许多地方。

解决方案:

这里的关键是 $string 只包含真正的子字符串 ($pattern),需要用其他子字符串 ($replacement) 替换。正则表达式可能是矫枉过正。虽然我确实有多个需要替换的子字符串,但它们保证在单词边界上。还有一点需要注意的是,替换可能有一个子字符串,它是@patterns 中的先前模式。 例如:

 @patterns = ('small', 'blue') ; 
 %pattern2replacement = ( 'small' => 'big and blue', 'blue' => 'black') ;

即我们希望字符串 small poxbig and black pox 替换 因此,以下替代方案提供了巨大的运行时改进:

#Step1: Build complete replacement hash:
my %oneshot_replacement ; 
foreach my $pattern (@patterns) {
  my $replacement = $pattern2replacement{$pattern} ;
  my @splits = split(/\b/, $replacement) ;
  @splits = map {exists $oneshot_replacement{$_} ? $oneshot_replacement{$_} : $_} @splits ;
  $oneshot_replacement{$pattern} = join("", @splits) ; 
}

#Step2: do substitution without regex:
foreach my $string (@strings) {
  my @splits = split(/\b/, $string) ;
  @splits = map {exists $oneshot_replacement{$_} ? $oneshot_replacement{$_} : $_} @splits ;
  $string = join("", @splits) ;
  # do something with $string
}

这有助于将运行时间从约 300 秒减少到约 20 秒。

【问题讨论】:

  • Re "我还能做些什么来减少这个运行时间?",您说您进行了 50 K 次调用,但输出显示正在进行 50 M 次调用。 /g 的每次迭代肯定被算作一个单独的替换。使用替换的定义,您每 6 纳秒进行一次替换!你真的不能去爸爸比。因此,更快的解决方案需要更好的方法。 (不是总是这样吗?)鉴于您没有提供有关您正在做什么的信息,我们无法为您提供帮助。
  • "不使用正则表达式功能的简单字符串" --- 但它已经完成了 50,000 次(或者是 50Ms?)。这是一个很大的开销,这是一个问题,启动引擎这么多次。当然,代码中可能还有其他未显示的开销。所以,如果你需要它更快,你需要一种不同的方法,就像往常一样;正如已经建议的那样。如果您要提出具体问题,我们可以多谈。
  • @ikegami - 我添加了更多细节以及解决方案(我在提出问题后意识到)。
  • 感谢 @zdim 的 cmets。为第一个不那么明确的问题道歉 - 我已经解决了 - 是的,它是 50M - 我的错。
  • 没问题,感谢您的回复。没有仔细看,但有一些直接的观察。 (1) 三元组(在map 中)可以替换为$h{$_} // $_(查询不存在的键返回undef// 是定义或)。 (2) 但是,@ary = map { ... } @ary; 复制了整个数组。为什么不exists $h{$_} and $_= $h{$_} } for @splits;,应该下注更快? (或者把它写成一个适当的循环,这样只会稍微慢一点)。如果哈希值不能是 0''(空字符串),您可以删除 exists

标签: regex perl optimization


【解决方案1】:

关于你的问题,剩下的 300 块都花在哪里了,答案可能是:在分析器中。

假设模式的数量“相对较少”,并且每个都是一个完整的单词,我猜这段代码比没有正则表达式的替换要快得多:

my $or = join("|",@patterns);
$string =~ s/\b($or)\b/$oneshot_replacement{$1}/g;
#print "$string";

无论如何,在第一个代码部分(#Step1:构建完整的替换哈希:)上面你犯了2个错误:

#my @splits = split(/\b/, $pattern) ;
my @splits = split(/\b/, $replacement) ;

如果您只想对模式数组进行一次迭代,则必须以正确的顺序执行此操作(如果可能的话)。

一种适用于您的示例的解决方案(允许某种扩展)是

#@splits = map {exists $oneshot_replacement{$_} ? $oneshot_replacement{$_} : $_} @splits ;
@splits = map {exists $oneshot_replacement{$_} ? $oneshot_replacement{$_} : exists $pattern2replacement{$_} ? $pattern2replacement{$_} : $_} @splits ;

【讨论】:

  • 感谢您在第一部分中发现错误!我编辑了它。关于正确的顺序 - 这些模式是从文件中解析的,并且要求文件按顺序指定模式。此外,我看不到您的修改将如何帮助解决真正无序的情况。它仍然没有考虑到连续需要进行替换(只做了一个步骤)。至于在分析器中花费的开销 - 这很难消化。如果运行时的分析器要报告它导致分析的开销,那么分析的目的几乎是失败的。
  • 修改将在一级替换上使用 for - 无论模式出现的顺序如何。要正确执行此操作,您可以添加某种递归。
猜你喜欢
  • 2017-09-18
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-01-30
  • 1970-01-01
相关资源
最近更新 更多