for vs 在 Bash 中查找

rub*_*o77 28 performance bash shell-script

循环文件时有两种方式:

  1. 使用for循环:

    for f in *; do
        echo "$f"
    done
    
    Run Code Online (Sandbox Code Playgroud)
  2. 使用find

    find * -prune | while read f; do 
        echo "$f"
    done
    
    Run Code Online (Sandbox Code Playgroud)

假设这两个循环会找到相同的文件列表,那么这两个选项在性能和处理上有什么区别?

Phi*_*hil 19

我在一个包含 2259 个条目的目录上尝试了这个,并使用了该time命令。

time for f in *; do echo "$f"; done(减去文件!)的输出是:

real    0m0.062s
user    0m0.036s
sys     0m0.012s
Run Code Online (Sandbox Code Playgroud)

time find * -prune | while read f; do echo "$f"; done(减去文件!)的输出是:

real    0m0.131s
user    0m0.056s
sys     0m0.060s
Run Code Online (Sandbox Code Playgroud)

我多次运行每个命令,以消除缓存未命中。这表明将它保持在bash(for i in ...) 中比使用find和管道输出 (to bash)更快

为了完整起见,我从 中删除了管道find,因为在您的示例中,它完全是多余的。just 的输出find * -prune是:

real    0m0.053s
user    0m0.016s
sys     0m0.024s
Run Code Online (Sandbox Code Playgroud)

此外,time echo *(输出不是换行符分隔,唉):

real    0m0.009s
user    0m0.008s
sys     0m0.000s
Run Code Online (Sandbox Code Playgroud)

在这一点上,我怀疑原因echo *更快是它没有输出这么多换行符,所以输出没有滚动那么多。让我们测试...

time find * -prune | while read f; do echo "$f"; done > /dev/null
Run Code Online (Sandbox Code Playgroud)

产量:

real    0m0.109s
user    0m0.076s
sys     0m0.032s
Run Code Online (Sandbox Code Playgroud)

time find * -prune > /dev/null产量:

real    0m0.027s
user    0m0.008s
sys     0m0.012s
Run Code Online (Sandbox Code Playgroud)

time for f in *; do echo "$f"; done > /dev/null产生:

real    0m0.040s
user    0m0.036s
sys     0m0.004s
Run Code Online (Sandbox Code Playgroud)

最后:time echo * > /dev/null产生:

real    0m0.011s
user    0m0.012s
sys     0m0.000s
Run Code Online (Sandbox Code Playgroud)

一些变化可以通过随机因素来解释,但似乎很清楚:

  • 输出很慢
  • 管道成本有点高
  • for f in *; do ...本身比 慢find * -prune,但对于上述涉及管道的结构,速度更快。

此外,顺便说一句,这两种方法似乎都可以很好地处理带有空格的名称。

编辑:

find . -maxdepth 1 > /dev/null比赛时间find * -prune > /dev/null

time find . -maxdepth 1 > /dev/null

real    0m0.018s
user    0m0.008s
sys     0m0.008s
Run Code Online (Sandbox Code Playgroud)

find * -prune > /dev/null

real    0m0.031s
user    0m0.020s
sys     0m0.008s
Run Code Online (Sandbox Code Playgroud)

所以,补充结论:

  • find * -prunefind . -maxdepth 1- 在前者中慢,shell 正在处理一个 glob,然后为find. 注意:find . -prune只返回..

更多测试 time find . -maxdepth 1 -exec echo {} \; >/dev/null

real    0m3.389s
user    0m0.040s
sys     0m0.412s
Run Code Online (Sandbox Code Playgroud)

结论:

  • 迄今为止最慢的方法。正如在建议这种方法的答案的评论中指出的那样,每个参数都会产生一个 shell。

  • @rubo77 `find * -prune | 读f时;做 echo "$f"; done` 有多余的管道 - 所有管道所做的就是准确地输出 `find` 自己输出的内容。没有管道,它只是简单的 `find * -prune` 管道只是多余的,因为管道另一侧的东西只是简单地将 stdin 复制到 stdout(在大多数情况下)。这是一个昂贵的空操作。如果你想对 find 的输出做一些事情,而不是把它再次吐出来,那是不同的。 (2认同)

Bit*_*Nix 10

我肯定会选择 find 虽然我会将您的 find 更改为:

find . -maxdepth 1 -exec echo {} \;
Run Code Online (Sandbox Code Playgroud)

性能方面,find当然要快得多,这取决于您的需求。您当前拥有的for只会显示当前目录中的文件/目录,而不是目录内容。如果您使用 find,它还会显示子目录的内容。

我说,发现是因为有更好的你for*将不得不首先被膨胀和我怕,如果你有一个目录与一个巨大的文件的数量可能会给错误过长参数列表。同样适用find *

例如,在我目前使用的其中一个系统中,有几个目录包含超过 200 万个文件(每个 <100k):

find *
-bash: /usr/bin/find: Argument list too long
Run Code Online (Sandbox Code Playgroud)

  • 这将为每个文件生成一个 echo 进程(而在 shell for 循环中,它是 echo 内置函数,将在不分叉额外进程的情况下使用),并将下降到目录中,因此它会慢很多。另请注意,它将包含点文件。 (4认同)

Sté*_*las 10

1.

第一个:

for f in *; do
  echo "$f"
done
Run Code Online (Sandbox Code Playgroud)

失败名为文件-n-e并像变种-nene,并与一些bash的部署,包含反斜杠的文件名。

第二:

find * -prune | while read f; do 
  echo "$f"
done
Run Code Online (Sandbox Code Playgroud)

在更多情况下失败(名为!, -H, -name, 的(文件名以空格开头或结尾或包含换行符...)

它的外壳,膨胀*find什么也不做,但打印接收到作为参数的文件。您也可以使用printf '%s\n'which as printfis builtin 也可以避免过多的 args潜在错误。

2.

的展开*是排序的,如果不需要排序可以快一点。在zsh

for f (*(oN)) printf '%s\n' $f
Run Code Online (Sandbox Code Playgroud)

或者干脆:

printf '%s\n' *(oN)
Run Code Online (Sandbox Code Playgroud)

bash据我所知,没有等效项,因此您需要求助于find.

3.

find . ! -name . -prune ! -name '.*' -print0 |
  while IFS= read -rd '' f; do
    printf '%s\n' "$f"
  done
Run Code Online (Sandbox Code Playgroud)

(以上使用 GNU/BSD-print0非标准扩展)。

这仍然涉及生成 find 命令并使用慢while read循环,因此for除非文件列表很大,否则它可能比使用循环慢。

4.

此外,与 shell 通配符扩展相反,find将对lstat每个文件进行系统调用,因此非排序不太可能对此进行补偿。

使用 GNU/BSD find,可以通过使用它们的-maxdepth扩展来避免这种情况,这将触发优化保存lstat

find . -maxdepth 1 ! -name '.*' -print0 |
  while IFS= read -rd '' f; do
    printf '%s\n' "$f"
  done
Run Code Online (Sandbox Code Playgroud)

因为find在找到文件名后立即开始输出文件名(stdio 输出缓冲除外),如果您在循环中执行的操作很耗时并且文件名列表不仅仅是一个 stdio 缓冲区(4 /8 KB)。在这种情况下,循环内的处理将在find完成查找所有文件之前开始。在 GNU 和 FreeBSD 系统上,您可以使用stdbuf使这种情况更快发生(禁用 stdio 缓冲)。

5.

为每个文件运行命令的 POSIX/标准/便携方式find是使用-exec谓词:

find . ! -name . -prune ! -name '.*' -exec some-cmd {} ';'
Run Code Online (Sandbox Code Playgroud)

在这种情况下echo,这比在 shell 中执行循环效率低,因为 shell 将具有内置版本的echowhilefind将需要生成一个新进程并/bin/echo在其中为每个文件执行。

如果您需要运行多个命令,您可以执行以下操作:

find . ! -name . -prune ! -name '.*' -exec cmd1 {} ';' -exec cmd2 {} ';'
Run Code Online (Sandbox Code Playgroud)

但请注意,cmd2只有在cmd1成功时才会执行。

6.

为每个文件运行复杂命令的规范方法是使用以下命令调用 shell -exec ... {} +

find . ! -name . -prune ! -name '.*' -exec sh -c '
  for f do
    cmd1 "$f"
    cmd2 "$f"
  done' sh {} +
Run Code Online (Sandbox Code Playgroud)

那个时候,我们又回到了高效的状态,echo因为我们使用sh的是内置的,并且-exec +版本生成的sh越少越好。

7.

对 ext4 上包含 200.000 个短名称文件的目录进行的测试中zsh一个(第 2 段)是迄今为止最快的,其次是第一个简单for i in *循环(尽管像往常一样,bash比其他 shell 慢得多)。


l0b*_*0b0 7

find * -prune | while read f; do 
    echo "$f"
done
Run Code Online (Sandbox Code Playgroud)

是一种无用的用法find- 您所说的实际上是“对于目录 ( *)中的每个文件,都找不到任何文件。此外,由于以下几个原因,它不安全:

  • 在路径中的反斜杠被特殊对待,而不-r选项read。这不是for循环的问题。
  • 路径中的换行符会破坏循环内的任何重要功能。这不是for循环的问题。

使用 处理任何文件名find困难,因此for仅出于这个原因,您应该尽可能使用循环选项。此外,运行外部程序find通常比运行内部循环命令(例如for.