【发布时间】: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 pox 被 big 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