为什么在使用grep -P时,^A|bA比负向回顾要慢,但在使用grep -E时却很快?

4

以下两种写法是等价的:

grep -E '^A|bA'
grep -P '^A|bA'
grep -P '(?<![^b])A'

但第二种方法 grep -P '^A|bA' 要慢得多。为什么呢?
它们都找到了同样的内容:一行开头或在一个 b 后面有一个 A。(等效地说,一行有一个没有被任何东西而只是一个 b 后面跟着的 A。)
第二行是否禁用了某些优化?当 grep 认为这样更快时,它是否会检查多个字符并行处理?除非 ^| 在 perl 中有微妙不同的含义,否则我想不出其他解释。

1
你能展示一些样例输入和基准数据吗?在我的电脑上运行时间大约是相同的,每次运行大约需要0.001秒。我尝试运行了10000次,得到了9.3秒和9.5秒的结果,差不多是相同的,没有变得多倍慢。 - xxfelixxx
1
perl -le'print "bcA" x 10 for 1..10_000' >neg生成一个文件,展示了OP提到的行为。 - ikegami
1
简短回答:为了支持Perl“正则”表达式的额外功能,必须使用一个不同但效率较低的引擎。请参见此链接 - ikegami
我正在Chrome源代码树上运行此程序,但任何文本文件集合似乎都可以工作。@ikegami的命令也可以使用(在提高到“1_000_000”后)。 - mgiuffrida
2个回答

6

如果模式中不含反向引用,GNU egrep (grep -E)会使用DFA引擎; grep -P使用PCRE的NFA实现。DFA引擎永远不会回溯,而模式^A|bA可能会触发PCRE中大量低效的回溯。

PCRE在每个字符串位置检查^A,然后是bA,直到找到匹配项。对于没有匹配项或者在字符串末尾才有匹配项的大型输入,这可能需要很长时间。

您可以通过pcretest工具来查看此过程:

$ pcretest
PCRE version 8.32 2012-11-30

  re> /^A|bA/C
data> bcAbcAbcA
--->bcAbcAbcA
 +0 ^             ^
 +1 ^             A
 +3 ^             b
 +4 ^^            A
 +0  ^            ^
 +3  ^            b
 +0   ^           ^
 +3   ^           b
 +0    ^          ^
 +3    ^          b
 +4    ^^         A
 +0     ^         ^
 +3     ^         b
 +0      ^        ^
 +3      ^        b
 +0       ^       ^
 +3       ^       b
 +4       ^^      A
 +0        ^      ^
 +3        ^      b
 +0         ^     ^
 +3         ^     b
No match

(?<![^b])A之所以更快,是因为PCRE会直接跳到第一个A进行匹配,而不是在每个位置都测试匹配;如果第一个A不匹配,则跳到下一个A,一直到字符串的末尾:

  re> /(?<![^b])A/C
data> bcAbcAbcA
--->bcAbcAbcA
 +0   ^           (?<![^b])
 +4   ^      ^    [^b]
 +8   ^           )
 +0      ^        (?<![^b])
 +4      ^   ^    [^b]
 +8      ^        )
 +0         ^     (?<![^b])
 +4         ^^    [^b]
 +8         ^     )
 +0          ^    (?<![^b])
 +4          ^    [^b]
 +8          ^    )
No match

关于DFA和NFA实现之间的差异的详细信息,请参阅Russ Cox的文章“正则表达式匹配可以简单快速”。


* 根据Jeffrey Friedl的《精通正则表达式》第182页中的“DFA速度与NFA功能:正则表达式天堂?”


-1
使用-P选项时,grep可能表现不佳的原因在于使用了不同的正则表达式(pcre)引擎,该引擎在使用的算法上更加复杂。快速查看内部实现可以揭示出发生了什么(GNU grep 2.20): -E——扩展正则表达式
Starting program: /usr/bin/grep -Eq \^A\|bA wordlist.dic
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib64/libthread_db.so.1".

Breakpoint 1, 0x0000000000402ec0 in main ()
(gdb) si 200000
0x000000000040c667 in dfacomp ()
(gdb) bt
#0  0x000000000040c667 in dfacomp ()
#1  0x000000000040d618 in GEAcompile ()
#2  0x000000000040328a in main ()
(gdb) si 200000
0x00007ffff78410db in memchr () from /lib64/libc.so.6
(gdb) bt
#0  0x00007ffff78410db in memchr () from /lib64/libc.so.6
#1  0x000000000040f952 in kwsexec ()
#2  0x000000000040dc2d in EGexecute ()
#3  0x000000000040500a in grepbuf ()
#4  0x0000000000405b60 in grepdesc ()
#5  0x000000000040330f in main ()
(gdb) si 200000
[Inferior 1 (process 23706) exited normally]

-P --perl-regexp

Starting program: /usr/bin/grep -Pq \^A\|bA wordlist.dic
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib64/libthread_db.so.1".

Breakpoint 1, 0x0000000000402ec0 in main ()
(gdb) si 200000
0x00007ffff7835eed in _int_malloc () from /lib64/libc.so.6
(gdb) bt
#0  0x00007ffff7835eed in _int_malloc () from /lib64/libc.so.6
#1  0x00007ffff783826c in malloc () from /lib64/libc.so.6
#2  0x00007ffff7ba0fe4 in sljit_create_compiler () from /lib64/libpcre.so.1
#3  0x00007ffff7bbb32f in _pcre_jit_compile () from /lib64/libpcre.so.1
#4  0x00007ffff7bbfd8d in pcre_study () from /lib64/libpcre.so.1
#5  0x000000000041000e in Pcompile ()
#6  0x000000000040328a in main ()
(gdb) si 1200000
0x00007ffff7bbdc31 in _pcre_jit_exec () from /lib64/libpcre.so.1
(gdb) bt
#0  0x00007ffff7bbdc31 in _pcre_jit_exec () from /lib64/libpcre.so.1
#1  0x00007ffff7b9f083 in pcre_exec () from /lib64/libpcre.so.1
#2  0x0000000000410372 in Pexecute ()
#3  0x000000000040500a in grepbuf ()
#4  0x0000000000405b60 in grepdesc ()
#5  0x000000000040330f in main ()
... and still going ...

正如您所看到的,调用pcre引擎时会发生更多的事情,而不仅仅是内置的内容。基本上,当使用此正则表达式选项时,grep需要执行超过四倍的指令来搜索相同的模式。


1
引擎是“外部”还是“本地”(这到底意味着什么)并不重要。堆栈跟踪的深度也不重要。重要的是所使用的引擎类型(DFA vs NFA),请查看其他答案。 DFA引擎将在O(n)中完成其工作,其中n =输入大小。 NFA引擎的执行时间取决于模式的复杂性。执行DFA的代价是功能集的减少。 - Lucas Trzesniewski
2
这里没有额外的过程。pcre_exec只是PCRE引擎的入口点,与exec无关。而_pcre_jit_exec是一个内部函数,它执行PCRE从您的模式中编译在内存中的本机机器代码。 - Lucas Trzesniewski
这是PCRE的内部函数,不是grep。而且这个函数来自哪里并不重要,最终重要的是算法做什么 - Lucas Trzesniewski
@ikegami:我想这就是我的回答引起混淆的地方。另外,你有关于库只在进程中加载一次的来源吗(我只是好奇)?还有你所说的“更深层嵌套”是什么意思?谢谢。 - l'L'l
展示更多的指令被执行只是证明它是一个CPU密集型函数,但这并不应该让任何人感到惊讶。我认为没有人认为 -P 由于磁盘使用、线程锁定等原因而变慢,这使得更多的指令被执行显而易见。问题是为什么会这样。 - ikegami
显示剩余4条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接