为什么 grep/-r/--include 组合比 find/-exec/grep 组合慢?

为什么 grep/-r/--include 组合比 find/-exec/grep 组合慢?

据我了解,以下两个命令大致完成同样的事情:

命令 1:

find -name "filename.xml" -exec grep someString {} \;

命令2:

grep -r --include=filename.xml someString .

尽管如此,在相同环境下热身后计时,第一个比第二个快大约 3 倍(大约 4 秒对 12 秒)。

我测试的文件夹树中与文件名模式匹配的文件数量非常少,而且每个文件都非常小。这让我认为大部分时间都花在了查找与文件名模式匹配的文件中,而不是在查找这些匹配的文件上。

那么为什么这两条命令行的性能差异如此之大?

答案1

事实上恰恰相反;grep 命令通常更高效。

我将致力于研究 Gentoo 的 Portage 树快照,如果您想尝试的话,它是公开可用的。

 $ time find /usr/portage/sys-apps/ -name '*.ebuild' -exec grep DEPEND {} \; > /dev/null

real    0m1.184s
user    0m0.033s
sys     0m0.130s

 $ time grep -r --include '*.ebuild' DEPEND /usr/portage/sys-apps/ > /dev/null

real    0m0.017s
user    0m0.007s
sys     0m0.010s

让我们看一下每个函数被调用得最多的是哪些函数:

 $ (strace find /usr/portage/sys-apps/ -name '*.ebuild' -exec grep DEPEND {} \; > /dev/null) |& sed 's/[({].*//g' | sort | uniq -c | sort -r | head -n 10
   3574 fcntl
   1597 close
    794 newfstatat
    794 getdents
    689 wait4
    689 clone
    689 --- SIGCHLD 
    404 fstat
    397 openat
     20 mmap

 $ (strace grep -r --include '*.ebuild' DEPEND /usr/portage/sys-apps/ > /dev/null) |& sed 's/[({].*//g' | sort | uniq -c | sort -r | head -n 10
   2779 fcntl
   1493 close
   1382 read
   1096 fstat
   1087 openat
    794 getdents
    792 newfstatat
    691 ioctl
    689 lseek
     25 write

再看看那些长时间的通话:

 $ (strace -T find /usr/portage/sys-apps/ -name '*.ebuild' -exec grep DEPEND {} \; > /dev/null) |& sed 's/\(.*\)<\(.*\)>/\2 \1/g' | sort -nk1r | head -n10
exit_group(0)                           = ?
0.001884 wait4(29725, [{WIFEXITED(s) && WEXITSTATUS(s) == 0}], 0, NULL) = 29725 
0.001879 wait4(29475, [{WIFEXITED(s) && WEXITSTATUS(s) == 0}], 0, NULL) = 29475 
0.001813 wait4(29430, [{WIFEXITED(s) && WEXITSTATUS(s) == 1}], 0, NULL) = 29430 
0.001812 wait4(30089, [{WIFEXITED(s) && WEXITSTATUS(s) == 0}], 0, NULL) = 30089 
0.001807 wait4(29722, [{WIFEXITED(s) && WEXITSTATUS(s) == 1}], 0, NULL) = 29722 
0.001795 wait4(29645, [{WIFEXITED(s) && WEXITSTATUS(s) == 0}], 0, NULL) = 29645 
0.001794 wait4(29848, [{WIFEXITED(s) && WEXITSTATUS(s) == 1}], 0, NULL) = 29848 
0.001759 wait4(30032, [{WIFEXITED(s) && WEXITSTATUS(s) == 0}], 0, NULL) = 30032 
0.001754 wait4(30093, [{WIFEXITED(s) && WEXITSTATUS(s) == 0}], 0, NULL) = 30093

 $ (strace -T grep -r --include '*.ebuild' DEPEND /usr/portage/sys-apps/ > /dev/null) |& 
exit_group(0)                           = ?
0.002336 fcntl(3, F_SETFD, FD_CLOEXEC)           = 0 
0.000460 read(3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\200\30`C6\0\0\0"..., 832) = 832 
0.000313 close(3)                                = 0 
0.000295 execve("/bin/grep", ["grep", "-r", "--include", "*.ebuild", "DEPEND", "/usr/portage/sys-apps/"], [/* 75 vars */]) = 0 
0.000276 fcntl(3, F_SETFD, FD_CLOEXEC)           = 0 
0.000265 getdents(3, /* 244 entries */, 32768)   = 7856 
0.000233 fstat(3, {st_mode=S_IFREG|0644, st_size=826, ...}) = 0 
0.000162 open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3 
0.000137 lseek(3, 1402, 0x4 /* SEEK_??? */)      = -1 ENXIO (No such device or address) 

非常有趣,在这个持续时间输出中,你会看到 find 等待了很长时间,而 grep 执行了一些启动和停止进程所需的操作。wait 调用花费了超过 0.001 秒的时间,而 find 调用则减少到稳定的 ~0.0002 秒。

如果你看一下 count 输出中的 wait4 调用,你会注意到发生了相等数量的 clone 调用和 SIGCHLD 信号;这是因为 find 会为遇到的每个文件调用 grep 进程,这就是它的效率受到影响的地方,因为克隆和等待的成本很高。

有些时候它不会受到影响;您可能会得到一组足够小的文件,因此启动多个 grep 进程的开销不会太大,但您的磁盘也可能非常慢,以至于忽略了启动新进程的开销,而且可能还有其他原因。不过,在比较速度时,我们通常会考虑一种或另一种方法的扩展性,而不是考虑特殊的极端情况。

在您的案例中,您提到“这就是为什么我觉得“grep”访问目录树的方式与“find”相比效率低下。",情况可能确实如此;如您所见,已经进行了 1382 次读取调用,而 find 并没有这样做,这会使 grep 的 I/O 消耗更大。

总结:了解原因你的时间效率低下,尝试再次进行此分析,并找出您案例中的问题,以便您知道为什么您的特定数据和任务在 grep 中效率低下;您会发现不同的 grep 在您的极端情况下会有怎样的行为......

因此,正如其他人建议的那样,您需要确保它不会为每个文件调用 grep,这可以通过\;+结尾附近替换来完成。

 $ time find /usr/portage/sys-apps/ -name '*.ebuild' -exec grep DEPEND {} + > /dev/null

real    0m0.027s
user    0m0.010s
sys     0m0.013s

如您所见,0.027 秒与 0.017 秒非常接近;差异主要归因于它仍然必须调用 find 和 grep,而不是仅调用 grep。或者如评论中所示,在某些系统上,它+允许您改进 grep。

答案2

用 Wakizashi 而不是 Katana 来削土豆皮可能更好,但这两种工具都不是适合这项工作的好工具。数字工具也是如此,请明智地使用它们。

这听起来可能像是一个空洞的建议,但在这种情况下,例如,对于 find 示例中的每个文件,grep 都会执行一次。从性能角度来看,这并不明智。如果您用 '+' 而不是 '\;' 替换 find 的结束参数,grep 将只对找到的所有文件运行一次。

在这种情况下,要确切回答这个问题,必须比较 grep 和 find 源代码的相关部分,看看哪个在匹配(查找)文件名方面更快。坦率地说,这超出了我的技能范围。

直观地说,find 被优化为在目录中查找文件,而 grep 被优化为在文件中查找字符串。此外,该--include选项应该适用于大写和小写文件,而 `-name

编辑:(我的发现是错误的)

对包含约 35,000 个文件的 doc 文件夹进行一些基本调查:

$ strace find . -name "moo" -exec grep a {} \+ 2>&1 |grep ^open |wc -l
4448

$ strace grep -r --include=moo  a . 2>&1 | grep ^open | wc -l
2289

find 组合打开了更多文件。这表明结果与您的发现相反。我做了一些基本的计时(像 Tom Wijsman 一样)。

DIR=imagemagick-6.7.8.7
$ findhtml $DIR |& top10    $ grephtml $DIR |& top10
  1617 mmap2                  316 read
  1176 fstat64                173 close
  1176 close                  164 fstat64
   735 open                   157 openat
   608 read                   148 ioctl
   588 mprotect                63 fcntl64
   441 brk                     25 getdents64
   294 munmap                  16 fstatat64
   294 ioctl                   11 mmap2
   147 write                    5 write
time: Real 0m2.0s           time: Real 0m0.3s

我发现 find strace 指向 /usr/lib/locale/locale-archive,但我并不确定其含义是什么。

相关内容