循环文件时有两种方法:
使用 -
for
循环:for f in *; do echo "$f" done
使用
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 将有一个内置版本的echo
while 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
存在几个缺陷,很可能无法找到您期望的所有文件:
- POSIX find 不保证接受多个路径参数。大多数
find
实现都会这样做,但您仍然不应该依赖它。 find *
当你击打时会破裂ARG_MAX
。for f in *
不会,因为ARG_MAX
适用于exec
,而不是内置函数。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