哪个是更高效的正则表达式?

时间:2010-11-24 23:43:55

标签: regex performance

我最近收到了一些求职者测试结果,其中一个人声称他们提供的解决方案更有效率(我不会说哪个,因为我不想影响答案)。毋庸置疑,我对此持怀疑态度,但我对RE编译器的内部工作原理并不了解,以便进行智能评论。

问题是:给出正则表达式来识别0到99之间的数字。

答案是:

[0-9]{1,2}
[0-9]?[0-9]
[0-9]|([0-9][0-9])

我感兴趣的是为什么其中任何一个更快(或以其他任何方式更好)。提供证据而非猜想的奖励点,但如果你说得足够令人信服,我仍会猜测: - )

7 个答案:

答案 0 :(得分:10)

表达式[0-9]{1,2}应该是我想象的最快,尽管它取决于具体的引擎。

我的理由是:

  • [0-9] {1,2} - 这完全描述了您想要匹配的内容。
  • [0-9]?[0-9] - 如果第一场比赛失败,这可能会导致回溯。
  • [0-9] |([0-9] [0-9]) - 如果失败则需要检查第一个字符两次,这里的括号是不必要的并导致不必要的捕获。

以下是我在.NET中测试时的每秒迭代次数(没有RegexOptions.Compiled):

Regex                      100% valid input   50% valid input  100% invalid input
"^[0-9]{1,2}$"             749086             800313           870748
"^[0-9]?[0-9]$"            731951             725984           740152
"^(?:[0-9]|([0-9][0-9]))$" 564654             687248           870378

使用RegexOptions.Compiled:

Regex                      100% valid input   50% valid input  100% invalid input
"^[0-9]{1,2}$"             1486212            1592535          1831843
"^[0-9]?[0-9]$"            1301557            1448812          1559193
"^(?:[0-9]|([0-9][0-9]))$" 1131179            1303213          1394146

如图:

alt text

注意:我修改了每个正则表达式以要求完全匹配而不是执行搜索。

答案 1 :(得分:7)

理论上至少 ,像这样的相同的正则表达式将产生相同的自动机。基于DFA的匹配器将一次匹配一个字符并且在其状态中编码不同的可能分支(而不是一次取一个分支然后在失败时回溯),因此每个分支的性能将是相同的

这三个正则表达式将与此DFA匹配:

+---+  0-9  +---+  0-9  +---+  *  .---.
| A |  -->  | B |  -->  | C | --> (ERR)
+---+       +---+       +---+     '---'
  |          / \          |
  | *     * /   \ $       | $ 
  V        /     \        V
.---.     /       \     .---.
(ERR) <--'         '--> (ACC)
'---'                   '---'

状态A :启动状态。如果它看到一个数字,则转到B,否则转到ERROR状态 状态B :到目前为止匹配一位数。 EOL($)已被接受。数字移动到C.其他任何内容都是错误 状态C :两位数匹配。 EOL已被接受,其他任何内容都是错误的。

这是我的语言理论答案。我不能说现实世界的正则表达式引擎实现。我忽略了括号的捕获语义,因为我猜这不是问题的关键。 Automata也不处理其他“非理论”结构,如贪婪,先行等。至少在他们的教科书演示中没有。

答案 2 :(得分:4)

在不知道正则表达式引擎的情况下,人们甚至无法确定这些是否正确。

例如,POSIX ERE是最左边的,而不是最左边的,所以它会选择一系列替代中最长的,因此选择匹配"ab"的字符串与/a|ab/匹配整个字符串"ab"。但是正常的回溯NFA你最常见的方式就是在那里做其他的事情:它会关心排序,因此将相同的"ab"字符串与相同的/a|ab/模式相匹配将只选择开头部分,"a"

下一个问题是相同模式的捕获组。如果它们是故意的,那么它们很奇怪,因为你保留两位数而不是一位数字。其他模式不这样做,但据说它们的行为相同。所以我假设他们在这里犯了错误。否则,捕获组的内存使用当然会花费更多而不是而不是这样做。

下一个问题是缺乏任何锚点。同样,我们无法知道它们是否正确,因为不清楚输入集的外观是什么,也不清楚特定引擎对非锚定模式的作用。大多数引擎会搜索字符串中的任何位置,但一些程序员友好程度较低的引擎将“有用”地添加行首(BOL)和行尾(EOL)锚点。在更常见的引擎中,如果没有这种情况,行中间的邮政编码也会匹配,因为五位数字显然包含一位和两位数的子串。无论你想要^$锚点,还是\b锚点,我无法猜测。

所以我必须在这里做一些猜测。我要离开锚点,但是我要重新排序第三个版本的分支,因为否则你永远不能将两位数字与正常(非POSIX)类型的回溯NFA匹配,大多数事情都会运行。< / p>

在考虑时序之前,看看正则表达式编译器从这些模式构建的程序真的是值得的。

% perl -Mre=debug -ce '@pats = ( qr/[0-9]{1,2}/, qr/[0-9]?[0-9]/, qr/[0-9][0-9]|[0-9]/ )'
Compiling REx "[0-9]{1,2}"
Final program:
   1: CURLY {1,2} (14)
   3:   ANYOF[0-9][] (0)
  14: END (0)
stclass ANYOF[0-9][] minlen 1 
Compiling REx "[0-9]?[0-9]"
synthetic stclass "ANYOF[0-9][]".
Final program:
   1: CURLY {0,1} (14)
   3:   ANYOF[0-9][] (0)
  14: ANYOF[0-9][] (25)
  25: END (0)
stclass ANYOF[0-9][] minlen 1 
Compiling REx "[0-9][0-9]|[0-9]"
Final program:
   1: BRANCH (24)
   2:   ANYOF[0-9][] (13)
  13:   ANYOF[0-9][] (36)
  24: BRANCH (FAIL)
  25:   ANYOF[0-9][] (36)
  36: END (0)
minlen 1 
-e syntax OK
Freeing REx: "[0-9]{1,2}"
Freeing REx: "[0-9]?[0-9]"
Freeing REx: "[0-9][0-9]|[0-9]"

查看已编译的模式确实是个好主意。观察正在执行的编译模式可能更具指导性。在这里,我们将同时观察:

% perl -Mre=debug -e '"aabbbababbaaqcccaaaabcacabba" =~ /abc|bca|cab|caab|baac|bab|aaa|bbb/'
Compiling REx "abc|bca|cab|caab|baac|bab|aaa|bbb"
Final program:
   1: TRIEC-EXACT[abc] (25)
      <abc> 
      <bca> 
      <cab> 
      <caab> 
      <baac> 
      <bab> 
      <aaa> 
      <bbb> 
  25: END (0)
stclass AHOCORASICKC-EXACT[abc] minlen 3 
Matching REx "abc|bca|cab|caab|baac|bab|aaa|bbb" against "aabbbababbaaqcccaaaabcacabba"
Matching stclass AHOCORASICKC-EXACT[abc] against "aabbbababbaaqcccaaaabcacabba" (28 chars)
   0 <> <aabbbababb>         | Charid:  1 CP:  61 State:    1, word=0 - legal
   1 <a> <abbbababba>        | Charid:  1 CP:  61 State:    2, word=0 - legal
   2 <aa> <bbbababbaa>       | Charid:  2 CP:  62 State:   11, word=0 - fail
   2 <aa> <bbbababbaa>       | Fail transition to State:    2, word=0 - legal
   3 <aab> <bbababbaaq>      | Charid:  2 CP:  62 State:    3, word=0 - fail
   3 <aab> <bbababbaaq>      | Fail transition to State:    5, word=0 - legal
   4 <aabb> <bababbaaqc>     | Charid:  2 CP:  62 State:   13, word=0 - legal
   5 <aabbb> <ababbaaqcc>    | Charid:  1 CP:  61 State:   14, word=8 - accepting
Matches word #8 at position 2. Trying full pattern...
   2 <aa> <bbbababbaa>       |  1:TRIEC-EXACT[abc](25)
   2 <aa> <bbbababbaa>       |    State:    1 Accepted:    0 Charid:  2 CP:  62 After State:    5
   3 <aab> <bbababbaaq>      |    State:    5 Accepted:    0 Charid:  2 CP:  62 After State:   13
   4 <aabb> <bababbaaqc>     |    State:   13 Accepted:    0 Charid:  2 CP:  62 After State:   14
   5 <aabbb> <ababbaaqcc>    |    State:   14 Accepted:    1 Charid:  8 CP:   0 After State:    0
                                  got 1 possible matches
                                  only one match left: #8 <bbb>
   5 <aabbb> <ababbaaqcc>    | 25:END(0)
Match successful!
Freeing REx: "abc|bca|cab|caab|baac|bab|aaa|bbb"

在这里,编译器对我们进行了非常聪明,并将其编译成Aho-Corasick trie结构。显然,这与正常的回溯NFA在同一个程序中的表现完全不同。

无论如何,这是您的模式的时间,或接近它们。我为第二个添加了另一个替代配方,我在三号中交换了替代品的排序。

testing against short_fail
                 Rate     second      first      third second_alt
second      9488823/s         --        -9%       -21%       -29%
first      10475308/s        10%         --       -13%       -22%
third      11998438/s        26%        15%         --       -11%
second_alt 13434377/s        42%        28%        12%         --

testing against long_fail
                 Rate     second      first      third second_alt
second     11221411/s         --        -3%        -5%        -5%
first      11618967/s         4%         --        -1%        -1%
third      11776451/s         5%         1%         --        -0%
second_alt 11786700/s         5%         1%         0%         --
testing against short_pass

                 Rate      first second_alt     second      third
first      11720379/s         --        -4%        -7%        -7%
second_alt 12199048/s         4%         --        -3%        -4%
second     12593191/s         7%         3%         --        -1%
third      12663378/s         8%         4%         1%         --

testing against long_pass
                 Rate      third     second      first second_alt
third      11135053/s         --        -1%        -5%        -8%
second     11221655/s         1%         --        -4%        -7%
first      11716924/s         5%         4%         --        -3%
second_alt 12042240/s         8%         7%         3%         --

这是由这个程序产生的:

#!/usr/bin/env perl
use Benchmark qw<cmpthese>;

$short_fail = "a" x   1;
$long_fail  = "a" x 600;

$short_pass = $short_fail . 42;
$long_pass  = $long_fail  . 42;

for my $name (qw< short_fail long_fail short_pass long_pass >) {   
    print "\ntesting against $name\n";
    $_ = $$name;    
    cmpthese 0 => {
        first       => '/[0-9]{1,2}/',
        second      => '/[0-9]?[0-9]/',
        second_alt  => '/[0-9][0-9]?/',
        third       => '/[0-9][0-9]|[0-9]/',
    }    
}

以下是添加了锚点的数字:

testing against short_fail
                 Rate      first     second second_alt      third
first      11720380/s         --        -3%        -4%       -11%
second     12058622/s         3%         --        -1%        -9%
second_alt 12180583/s         4%         1%         --        -8%
third      13217006/s        13%        10%         9%         --
testing against long_fail
                 Rate      third      first second_alt     second
third      11378120/s         --        -2%        -4%       -12%
first      11566419/s         2%         --        -2%       -10%
second_alt 11830740/s         4%         2%         --        -8%
second     12860517/s        13%        11%         9%         --
testing against short_pass
                 Rate     second      third second_alt      first
second     11540465/s         --        -5%        -5%        -7%
third      12093336/s         5%         --        -0%        -3%
second_alt 12118504/s         5%         0%         --        -2%
first      12410348/s         8%         3%         2%         --
testing against long_pass
                 Rate      first     second second_alt      third
first      11423466/s         --        -1%        -4%        -7%
second     11545540/s         1%         --        -3%        -7%
second_alt 11870086/s         4%         3%         --        -4%
third      12348377/s         8%         7%         4%         --

以下是产生第二组数字的最小修改程序:

#!/usr/bin/env perl
use Benchmark qw<cmpthese>;

$short_fail = 1  . "a";
$long_fail  = 1  . "a" x 600;

$short_pass =  2;
$long_pass  = 42;

for my $name (qw< short_fail long_fail short_pass long_pass >) {
    print "testing against $name\n";
    $_ = $$name;
    cmpthese 0 => {
        first       => '/^(?:[0-9]{1,2})$/',
        second      => '/^(?:[0-9]?[0-9])$/',
        second_alt  => '/^(?:[0-9][0-9]?)$/',
        third       => '/^(?:[0-9][0-9]|[0-9])$/',
    }
}

答案 3 :(得分:3)

如果一个人必须更快(可能取决于所使用的正则表达式引擎),那么显然我的视图中的第一个(可以是一个简单的Morris-Pratt表DFA与其他两个相比),另一个两个人可能需要回溯或执行额外的工作:

[0-9]?[0-9] - 对于一位数的情况,引擎将贪婪并匹配第一个数字,然后第二个数字失败;回溯然后成功

[0-9]|([0-9][0-9]) - 此处使用捕获组,这会减慢速度

答案 4 :(得分:3)

我对内部结构没有任何线索但是对于一些假长凳呢? :d

<强>的Python

import re
import time

regs = ["^[0-9]{1,2}$", "^[0-9]?[0-9]$", "^[0-9]|([0-9][0-9])$"]
numbers = [str(n) for n in range(0, 100)]

result = None

// determine loop overhead
start = time.time()
for e in xrange(0, 10000):
    for n in numbers:
        result = n

loop = time.time() - start


for i in regs:
    r = re.compile(i)
    now = time.time()
    for e in xrange(0, 10000):
        for n in numbers:
            result = r.search(n)

    print (time.time() - now) - loop

结果以秒为单位

0.874
0.869
0.809

<强>的JavaScript

var regs = ["^[0-9]{1,2}$", "^[0-9]?[0-9]$", "^[0-9]|([0-9][0-9])$"]

var numbers = [];
for(var n = 0; n < 100; n++) {
    numbers.push(''+n);
}


// determine loop overhead
var result = null;
var start = new Date().getTime();
for(var e = 0; e < 10000; e++) {
    for(var n = 0; n < 100; n++) {
        result = numbers[n];
    }
}

// test regex
var loop = new Date().getTime() - start;
for(var i = 0; i < regs.length; i++) {
    var r = new RegExp(regs[i]);
    var now = new Date().getTime();
    for(var e = 0; e < 10000; e++) {
        for(var n = 0; n < 100; n++) {
            result = r.exec(numbers[n]);
        }
    }
    console.log((new Date().getTime() - now) - loop); //using document.write here in Browsers
}

结果以秒为单位

Node.js
0.197
0.193
0.226

Opera 11
0.836
0.408
0.372

Firefox 4
2.039
2.491
2.488

那我们学到了什么?好吧Pythons似乎相当慢,V8似乎相当快 但是,嘿长椅总是很有趣!

更新:Java版本

import java.util.regex.Pattern;

public class Test {
    public static void main(String args[]) {
        test();
        test();
        test();
        test();
    }

    public static void test() {
        String regs[] = {"^[0-9]{1,2}$", "^[0-9]?[0-9]$", "^[0-9]|([0-9][0-9])$"};
        String numbers[] = new String[100];
        for(int n = 0; n < 100; n++) {
            numbers[n] = Integer.toString(n);
        }

        // determine loop overhead
        String nresult = "";
        long start = System.nanoTime();
        for(int e = 0; e < 10000; e++) {
            for(int n = 0; n < 100; n++) {
                nresult = numbers[n];
            }
        }

        long loop = System.nanoTime() - start;

        boolean result = false;
        for(int i = 0; i < regs.length; i++) {
            Pattern p = Pattern.compile(regs[i]);

            long now = System.nanoTime();
            for(int e = 0; e < 10000; e++) {
                for(int n = 0; n < 100; n++) {
                    result = p.matcher(numbers[i]).matches();
                }
            }
            System.out.println(((System.nanoTime() - now) - loop) / 1000000);
        }
        System.out.println(result);
        System.out.println(nresult);
    }
}

以秒为单位的结果(第4次运行的次数)

0.230
0.262
0.210

答案 5 :(得分:1)

那些正则表达式是如此微不足道,它应该无关紧要。但是,如果我必须选择一个更有效的实现,它将是[0-9] {1,2}或[0-9] [0-9]?,这不是你的选择,因为没有回溯必要的。

答案 6 :(得分:1)

就像C和++ii=i+1一样,一个好的正则表达式编译器应该将所有这三个编译成完全相同的有限自动机。如果没有,我会认为这是一个错误。

(例外:如果启用了带括号的子表达式标记,则第三个显然会编译以包含额外的标记信息。)