【问题标题】:Regex engine optimization: Is `(?:.)*` really more costly than `.*`?正则表达式引擎优化:`(?:.)*` 真的比 `.*` 更昂贵吗?
【发布时间】:2014-03-28 15:13:52
【问题描述】:

一些背景

我通常使用网站http://regex101.com 来测试我的正则表达式,它在PCRE 中提供了一个“调试器”功能,可以让您逐步了解正则表达式引擎在做什么。

当用.* 匹配一个随机字符串时,这个调试器告诉我引擎遵循3 steps 的常数。

当与(?:.)* 匹配时,它会根据长度宣布一个数字:66 steps 表示类似0123456789012345678901234567899

(?:.)* 真的比.* 贵吗?

似乎在后一种情况下,每次进入组都被认为是一个新的步骤,而在前一种情况下,.* 是一次应用的。

这是网站正在做的某种“改进”(试图避免显示无用的情况),还是与真正的内部正则表达式机制相匹配?如果是这样,幕后的想法是什么?

【问题讨论】:

  • 我不知道性能。这将特定于实施。但是(?:.)* 并没有比.* 提供任何好处,但它确实使你的语法更长。
  • 这个问题带有微优化的味道。虽然从纯粹的学术角度来看这很有趣,但我真诚地希望你不要假设(无论如何没有非常坚实的基础)这两种构造之间微小但可测量的差异将以任何方式提供切实的好处给你。
  • +1 到@Vector Gorgoth。在优化正则表达式模式时,您希望通过减少回溯的可能性来做到这一点,使其尽可能接近于零。这样您才能获得真正的性能提升。

标签: regex perl pcre


【解决方案1】:

您没有指定哪个引擎。您提到了 PCRE,但您也使用 Perl 进行了标记。


regex101 站点显示 PCRE 使用一项操作处理 .*,但这并不意味着一项操作比等效的 (?:.)* 生成的操作更快。只有基准测试才能说明问题,但.* 可能会由于开销较少而稍微快一些。


在 Perl 中,它们编译为完全相同的正则表达式程序(如下所示),因此它们的执行方式相同。

>perl -Mre=debug -e"'0123456789012345678901234567899' =~ /.*/"
Compiling REx ".*"
Final program:
   1: STAR (3)
   2:   REG_ANY (0)
   3: END (0)
anchored(MBOL) implicit minlen 0
Matching REx ".*" against "0123456789012345678901234567899"
   0 <> <0123456789>         |  1:STAR(3)
                                  REG_ANY can match 31 times out of 2147483647...
  31 <901234567899> <>       |  3:  END(0)
Match successful!
Freeing REx: ".*"

>perl -Mre=debug -e"'0123456789012345678901234567899' =~ /(?:.)*/"
Compiling REx "(?:.)*"
Final program:
   1: STAR (3)
   2:   REG_ANY (0)
   3: END (0)
anchored(MBOL) implicit minlen 0
Matching REx "(?:.)*" against "0123456789012345678901234567899"
   0 <> <0123456789>         |  1:STAR(3)
                                  REG_ANY can match 31 times out of 2147483647...
  31 <901234567899> <>       |  3:  END(0)
Match successful!
Freeing REx: "(?:.)*"

在这两种情况下,都会扫描字符串中不是换行符的字符,仅此而已。


请注意,无论采取多少“步骤”,都无法在恒定时间内完成。 . 不匹配换行符(没有/s),因此正则表达式引擎必须检查它即将匹配的每个字符以查看它是否是换行符。

【讨论】:

  • 谢谢,这更清楚了。我假设计算时间会随着步数线性增加,但我想确实没有理由。
  • @Robin,同样的工作需要完成,无论采取多少步骤(无论这意味着什么)。当然,会有差异(这就是他们决定创建快捷方式的原因),但不会很大。 |||这让我想起了一些问题:“我如何在不使用循环的情况下对每个 Foo 做一些事情?”您可以在函数中隐藏循环(例如map),但循环仍然存在。
【解决方案2】:

您可以使用pcretest 查看差异。 Here 是一个很好的教程。

您的第一个示例显然需要几个步骤,然后是第二个。在带有+ 的左侧,您可以看到模式中的位置,右侧是输入中的匹配位置。

1.) /.*/CD on str 0123456789012345678901234567899 带有调试修饰符CD

2.) /(?:.)*/CD 相同的字符串

也就是说,真正发生了什么。

【讨论】:

  • 感谢您的信息!所以似乎.* 确实有一个优化,使它的时间恒定......例如,它也适用于[^!]*。你知道它可以依赖什么原则吗?
  • @Robin, .* 不能在恒定时间内完成,因为.* 在 PCRE 和 Perl 中都表示 [^\n]*(除非您使用 /s)。必须检查以确保每个匹配的字符不是换行符。
  • @Robin 我刚刚为(?:.)* 计算了 65 步。可能是,您提到的站点使用 pcretest 来调试信息。对于.*,它说 3 个步骤,我可以在这个测试中计算两个步骤。它可以依靠什么原理?不知道内部优化是如何工作的。
【解决方案3】:

我编写了一个小的基准代码来测试这个场景。并且您的两个正则表达式在性能上几乎同时返回。所以不确定哪个更好。

但是,我已将您更改的正则表达式 (?:.)* 更改为 (.)* 并大大降低了性能。我相信这是因为group capturing。代码如下:

use Benchmark qw( cmpthese );

cmpthese(-3, {    
    '.*'     => '"kasdaskdhas dhaskdh askdhqwioeuweakjsdhasjdk asjdk ask" =~ /.*/',
    '(?:.)*' => '"kasdaskdhas dhaskdh askdhqwioeuweakjsdhasjdk asjdk ask" =~ /(?:.)*/',
    '(.)*'   => '"kasdaskdhas dhaskdh askdhqwioeuweakjsdhasjdk asjdk ask" =~ /(.)*/',
});

输出:

            Rate   (.)* (?:.)*     .*
(.)*   2305921/s     --   -34%   -35%
(?:.)* 3499870/s    52%     --    -1%
.*     3524871/s    53%     1%     --

.*(?:.)* 之间 1% 的差异是噪音,毫无意义。

【讨论】:

  • Re “所以不确定哪个更好。”两者都不。它们编译成相同的正则表达式程序。看我的回答。
  • @ikegami 这是我第一次使用基准模块编写代码:D!熟能生巧,总有一天我会变成benchmark
  • (如果您愿意,timethese 会给出时间而不是费率表)
  • (.)* 是一个不同的操作,所以时差并不让我感到惊讶,但其他两种解决方案似乎编译相同。感谢您提供基准,在这方面有数字感觉很好。
【解决方案4】:

网站似乎没有解释这些步骤是什么

这一定是一个优化问题。问题(?:.)* 真的比.*? 更昂贵” 取决于使用的正则表达式引擎,并且该站点不太可能使用内置于 perl 编译器/解释器中的 Perl 正则表达式引擎.无论它做什么使用的优化都选择忽略像(?:.)*这样在现实世界中不太可能发生的琐碎情况。

如果您需要您的正则表达式运行得更快,那么您应该使用Benchmark 来比较不同的模式,或者可能是Regexp::Optimizer,它会尝试将您的模式重写为一个等效的更快的模式,或者Regexp::Debugger 这将允许您看看幕后发生了什么。

但是请不要采取这些措施,除非您编写了一个功能清晰但执行速度不够快的程序,并且已经证明瓶颈是正则表达式匹配。正则表达式引擎完全用 C 语言编写,您不太可能通过更改其使用的正则表达式模式对代码的整体速度产生重大影响。

【讨论】:

  • 将其加载到 Regexp::Debugger 中是有启发性的。试一试。
  • 该站点正在使用 PHP 的 PCRE 进行调试。显然(?:.)* 不太可能发生,我简化了示例以使其更容易。一个例子是([^p]++|p(?!attern))*+:内部++ 真的相关吗?
  • 根据rxrx,模式(?:.)* 需要127 步才能遍历他的输入字符串。模式.* 需要三个步骤来完成相同的匹配。
  • @DavidO,这意味着.*rxrx 使用的任何引擎中都被实现为单个操作,但没有说明它与等效的(?:.)* 相比有多快。
【解决方案5】:

我不是这方面的专家,但据我所知,是的,/(?:.)*//(.)*//.*/ 更昂贵。

根据backtracking 上的 Perl 文档,

正则表达式匹配的一个基本特征包括 称为回溯的概念,目前(在需要时)由 所有常规的非占有性表达式量词,即 * , *? , + , +?、{n,m} 和 {n,m}?。回溯通常在内部进行优化,但此处概述的一般原则是有效的。

所以基本上,.* 在内部进行了优化,但我找不到说明如何的来源。

我还找到了另一个来源,Mastering Regular Expressions 的作者 Jeffrey Friedl 的 blog 帖子。

顺便说一下,我想我应该提一下 Perl 如何 有时会优化它处理正则表达式的方式。有时 它实际上执行的测试可能比我所描述的要少。或者它 也许某些测试比其他测试更有效(例如, /x*/ 经过内部优化,比 /(x)/ 或 /(?:x)/)。有时 Perl 甚至可以决定一个正则表达式可以 从不匹配有问题的特定字符串,因此将绕过测试 完全一致。

如果其他人可以更详细地解释 Perl 所做的优化,那将很有用!

【讨论】:

  • 我想 Perl 的正则表达式编译器中的优化太深奥了,除了代码本身之外无法解释,而且它从一个版本的 Perl 到另一个版本不同。正则表达式已经是一种晦涩难懂的语言,编写一个模式来考虑编译器将执行的优化只会让它们更加如此。 Perl 正则表达式引擎快速,你应该让它做它的事情。
  • 如果您真的对正则表达式编译器所做的优化感兴趣,请查看the regcomp.c source here。是17000行C
猜你喜欢
  • 1970-01-01
  • 2016-04-13
  • 1970-01-01
  • 2013-08-08
  • 1970-01-01
  • 2012-01-30
  • 1970-01-01
相关资源
最近更新 更多