为什么这个正则表达式在 Java 中这么慢?

Ant*_*tek 50 python java regex perl performance

我最近有一个SonarQube规则(https://rules.sonarsource.com/java/RSPEC-4784)引起了我的注意,一些性能问题可以用作对 Java 正则表达式实现的拒绝服务。

事实上,以下 Java 测试显示了错误的正则表达式的速度有多慢:

    import org.junit.Test;

    public class RegexTest {

    @Test
    public void fastRegex1() {
        "aaaaaaaaaaaaaaaaaaaaaaaaaaaabs".matches("(a+)b");
    }

    @Test
    public void fastRegex2() {
        "aaaaaaaaaaaaaaaaaaaaaaaaaaaab".matches("(a+)+b");
    }

    @Test
    public void slowRegex() {
        "aaaaaaaaaaaaaaaaaaaaaaaaaaaabs".matches("(a+)+b");
    }
}
Run Code Online (Sandbox Code Playgroud)

如您所见,前两个测试很快,第三个测试非常慢(在 Java 8 中)

在此处输入图片说明

然而,Perl 或 Python 中的相同数据和正则表达式一点也不慢,这让我想知道为什么这个正则表达式在 Java 中的计算速度如此之慢。

$ time perl -e '"aaaaaaaaaaaaaaaaaaaaaaaaaaaabs" =~ /(a+)+b/ && print "$1\n"'
aaaaaaaaaaaaaaaaaaaaaaaaaaaa

real    0m0.004s
user    0m0.000s
sys     0m0.004s

$ time python3 -c 'import re; m=re.search("(a+)+b","aaaaaaaaaaaaaaaaaaaaaaaaaaaabs"); print(m.group(0))'
aaaaaaaaaaaaaaaaaaaaaaaaaaaab

real    0m0.018s
user    0m0.015s
sys     0m0.004s

Run Code Online (Sandbox Code Playgroud)

数据中额外的匹配修饰符+或尾随字符s是什么导致这个正则表达式如此缓慢,为什么它只特定于 Java?

And*_*ner 54

警告:我对正则表达式的内部结构知之甚少,这真的是猜测。而且我无法回答为什么 Java 会受此影响,但其他人不会(而且,当我运行它时,它比在 jshell 11 中的 12 秒快得多,因此它可能只影响某些版本)。

"aaaaaaaaaaaaaaaaaaaaaaaaaaaabs".matches("(a+)+b")
Run Code Online (Sandbox Code Playgroud)

许多as 可以通过多种方式匹配:

(a)(a)(a)(a)
(aa)(a)(a)
(a)(aa)(a)
(aa)(aa)
(a)(aaa)
etc.
Run Code Online (Sandbox Code Playgroud)

对于输入 string "aaaaaaaaaaaaaaaaaaaaaaaaaaaab",它将a在一次传递中贪婪地匹配所有这些s,匹配b,job done。

对于"aaaaaaaaaaaaaaaaaaaaaaaaaaaabs",当它到达末尾并发现字符串不匹配时(因为s),它没有正确识别出s永远无法匹配的意思。因此,经过并可能匹配为

(aaaaaaaaaaaaaaaaaaaaaaaaaaaa)bs
Run Code Online (Sandbox Code Playgroud)

它认为“哦,也许它失败了,因为我将as分组的方式- 然后回去尝试as 的所有其他组合。

(aaaaaaaaaaaaaaaaaaaaaaaaaaa)(a)bs  // Nope, still no match
(aaaaaaaaaaaaaaaaaaaaaaaaaa)(aa)bs  // ...
(aaaaaaaaaaaaaaaaaaaaaaaaa)(aaa)bs  // ...
...
(a)(aaaaaaaaaaaaaaaaaaaaaaaaaaa)bs  // ...
(aaaaaaaaaaaaaaaaaaaaaaaaaa(a)(a)bs  // ...
(aaaaaaaaaaaaaaaaaaaaaaaaa(aa)(a)bs  // ...
(aaaaaaaaaaaaaaaaaaaaaaaa(aaa)(a)bs  // ...
...
Run Code Online (Sandbox Code Playgroud)

有很多这样的(我认为有类似 2^27 - 即 134,217,728 - 28a秒的组合,因为每个a都可以是前一组的一部分,也可以开始自己的组),所以需要很长时间。

  • “我无法回答为什么 Java 会遇到这个问题,而其他的却不会” Python 的性能同样很差(也许更糟)。只是 Python 的“搜索”并没有做与 Java 的“匹配”相同的事情。 (4认同)
  • 关于“*我无法回答为什么 Java 会受到这个问题而不是其他问题*”,那么,对于初学者来说, `"aaaaaaaaaaaaaaaaaaaaaaaaaaaabs" =~ /(a+)+b/` 匹配没有任何回溯。应该使用`/^(a+)+b\z/`。也就是说,“/^(a+)+b\z/”也非常快,因为优化器立即查找“ab<EOS>”,并意识到该模式不可能匹配。你可以看到使用 `perl -Mre=debug -e '"aaaaaaaaaaaaaaaaaaaaaaaaaaaabs" =~ /^(a+)+b\z/'` (3认同)
  • 关于。“这确实是猜想”:这基本上是正确的。这就是 Java 正则表达式的工作原理。尽管尝试的顺序不太正确。 (2认同)

Jim*_*mes 19

我不太了解 Perl,但 Python 版本并不等同于 Java 版本。您正在使用,search()但 Java 版本正在使用matches(). Python 中的等效方法是fullmatch()

当我在 Python (3.8.2) 中运行您的示例时,search()我会像您一样快速获得结果。当我运行它时,fullmatch()我的执行时间很差(多秒)。难道您的 Perl 示例也没有进行完整匹配?

顺便说一句:如果您想尝试搜索的 Java 版本,您可以使用:

Pattern.compile("(a+)+b").matcher("aaaaaaaaaaaaaaaaaaaaaaaaaaaabs").find();
Run Code Online (Sandbox Code Playgroud)

语义上可能有一些细微差别,但它应该足够接近此目的。

  • 回复“*您的 Perl 示例是否也没有进行完整匹配?*”,正确。Perl 等效项是“/^(a+)+b\z/”(而不是“/^(a+)+b$/”)。也就是说,优化器在开始匹配之前就意识到模式不可能匹配,然后中止。因此,与 Java 和 Python 不同,`"aaa...aaabs" =~ /^(a+)+b\z/` 会立即失败。您可以使用 `perl -Mre=debug -e '"aaaaaaaaaaaaaaaaaaaaaaaaaaaabs" =~ /^(a+)+b\z/'` 看到这一点(`没有找到浮动子字符串“ab”$...匹配被优化器拒绝` ) (8认同)
  • 在 Perl v5.30 中速度非常快:`echo aaaaaaaaaaaaaaaaaaaaaaaaaaaabs | perl -wne '/^((a+)+b)$/'` (2认同)

Hen*_*nry 14

+当字符串无法匹配时,额外的会导致大量回溯(在幼稚的正则表达式实现中)。如果字符串可以匹配,则在第一次尝试时就知道答案。这就解释了为什么情况 2 很快,而只有情况 3 很慢。


ica*_*rus 8

站点https://swtch.com/~rsc/regexp/regexp1.html有一些关于正则表达式实现技术及其背后理论的详细信息。我知道仅链接的答案很糟糕,但这值得一读,它展示了一个示例正则表达式,它在 30 微秒内完成,使用更好的实现,60 秒(慢 200 万倍)使用更广为人知和更明显的方式。

它说

“今天,正则表达式也成为了一个光辉的例子,说明忽视好的理论会导致糟糕的程序。今天流行的工具使用的正则表达式实现比许多 30 年前的 Unix 工具使用的要慢得多。”

其他答案说额外+导致太多回溯是正确的,但前提是您忽略了好的理论。

  • 使用 NFA 将遇到与更复杂的算法“完全相同的问题”。(具有 n 个状态的 NFA 最多可以有 2^n 条路径通过)。您必须了解该论文中使用的特定病态 RE,为什么 NFA 在该 RE 上更快,以及为什么这不适用于一般情况。 (4认同)
  • 快速健全性检查 - 30 秒与 60 秒不是 200 万的因数(而不是 2000)吗? (2认同)