Bash 中的 for 与 find

Bash 中的 for 与 find

循环文件时有两种方法:

  1. 使用 -for循环:

    for f in *; do
        echo "$f"
    done
    
  2. 使用find

    find * -prune | while read f; do 
        echo "$f"
    done
    

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

答案1

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

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

real    0m0.062s
user    0m0.036s
sys     0m0.012s

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

real    0m0.131s
user    0m0.056s
sys     0m0.060s

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

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

real    0m0.053s
user    0m0.016s
sys     0m0.024s

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

real    0m0.009s
user    0m0.008s
sys     0m0.000s

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

time find * -prune | while read f; do echo "$f"; done > /dev/null

产量:

real    0m0.109s
user    0m0.076s
sys     0m0.032s

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

real    0m0.027s
user    0m0.008s
sys     0m0.012s

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

real    0m0.040s
user    0m0.036s
sys     0m0.004s

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

real    0m0.011s
user    0m0.012s
sys     0m0.000s

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

  • 输出速度慢
  • 管道成本有点
  • 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

find * -prune > /dev/null:

real    0m0.031s
user    0m0.020s
sys     0m0.008s

所以,补充结论:

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

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

real    0m3.389s
user    0m0.040s
sys     0m0.412s

结论:

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

答案2

1.

第一个:

for f in *; do
  echo "$f"
done

-n对于名为和的文件以及某些 bash 部署中-e的变体-nene(文件名包含反斜杠)会失败。

第二:

find * -prune | while read f; do 
  echo "$f"
done

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

它是扩展的 shell *find除了打印它作为参数接收的文件之外什么也不做。您也可以使用内置printf '%s\n'的来代替,printf也可以避免参数太多潜在的错误。

2.

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

for f (*(oN)) printf '%s\n' $f

或者简单地:

printf '%s\n' *(oN)

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

3.

find . ! -name . -prune ! -name '.*' -print0 |
  while IFS= read -rd '' f; do
    printf '%s\n' "$f"
  done

(上面使用了 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

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

5.

为每个文件运行命令的 POSIX/标准/可移植方法find是使用-exec谓词:

find . ! -name . -prune ! -name '.*' -exec some-cmd {} ';'

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

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

find . ! -name . -prune ! -name '.*' -exec cmd1 {} ';' -exec cmd2 {} ';'

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

6.

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

find . ! -name . -prune ! -name '.*' -exec sh -c '
  for f do
    cmd1 "$f"
    cmd2 "$f"
  done' sh {} +

到那时,我们又恢复了高效,echo因为我们使用的是sh内置版本,并且生成的版本尽可能-exec +少。sh

7.

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

答案3

我肯定会选择 find,尽管我会将您的 find 更改为:

find . -maxdepth 1 -exec echo {} \;

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

我说 find 更好,因为对于你来说,for必须*首先扩展,而且我担心如果你有一个包含大量文件的目录,它可能会给出错误参数列表太长。同样适用于find *

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

find *
-bash: /usr/bin/find: Argument list too long

答案4

但我们对性能问题很着迷!这个实验请求至少做出了两个假设,使其不太有效。

A. 假设他们找到相同的文件……

嗯,他们将要首先找到相同的文件,因为它们都在同一个 glob 上迭代,即*.但find * -prune | while read f存在几个缺陷,很可能无法找到您期望的所有文件:

  1. POSIX find 不保证接受多个路径参数。大多数find实现都会这样做,但您仍然不应该依赖它。
  2. find *当你击打时会破裂ARG_MAXfor f in *不会,因为ARG_MAX适用于exec,而不是内置函数。
  3. while read f可以打破以空格开头和结尾的文件名,这些空格将被删除。您可以使用while read其默认参数来克服这个REPLY问题,但是当涉及到其中包含换行符的文件名时,这仍然对您没有帮助。

B echo..没有人会仅仅为了回显文件名而这样做。如果您想要这样做,只需执行以下操作之一:

printf '%s\n' *
find . -mindepth 1 -maxdepth 1 # for dotted names, too

此处循环的管道while创建了一个隐式子 shell,该子 shell 在循环结束时关闭,这对某些人来说可能不直观。

为了回答这个问题,以下是我的目录中的结果,其中包含 184 个文件和目录。

$ time bash -c 'for i in {0..1000}; do find * -prune | while read f; do echo "$f"; done >/dev/null; done'

real    0m7.998s
user    0m5.204s
sys 0m2.996s
$ time bash -c 'for i in {0..1000}; do for f in *; do echo "$f"; done >/dev/null; done'

real    0m2.734s
user    0m2.553s
sys 0m0.181s
$ time bash -c 'for i in {0..1000}; do printf '%s\n' * > /dev/null; done'

real    0m1.468s
user    0m1.401s
sys 0m0.067s

$ time bash -c 'for i in {0..1000}; do find . -mindepth 1 -maxdepth 1 >/dev/null; done '

real    0m1.946s
user    0m0.847s
sys 0m0.933s

相关内容