为什么从文件读取时'for'循环比'while'循环更快?

4

前言:

我一直被教导在shell中工作时,最好使用while循环而不是for循环,并且不要使用for循环与命令替换cat文件。我的理解是有许多原因,包括:

  • for循环需要一次性将所有要处理的数据加载到内存中
  • for循环默认情况下对空格进行单词分割,而不是换行符,因此除了必须将所有输入文件都放入内存中外,还会进行更多的单词分割从而占用更多的内存
  • for循环在您的in语句完成加载之前不会开始处理“do右侧”的内容,这意味着在等待结果的部分时间内,实际上没有任何事情正在发生,而是在“预加载”。

然而,在进行一些简单的测试后,我发现虽然内存消耗在for循环中似乎更大(这是可以预料的),但while循环的实际性能较低。这并不是一个巨大的差异,在任何现代计算机上可能开始变得重要的规模上,我可能会切换到awk或python,但我仍然很好奇为什么会发生这种情况。

测试设置:

我进行了一系列简单的测试,只是将文件中的行回显到/dev/null中。我的输入是包含100K和1Mil IP地址的两个平面文件。在下面的输出中,有一个测试,但我多次运行了此测试,并且每次结果都类似。我在2013 MBA(i7,8g Mem)上运行了此测试。

测试结果

Ds-MacBook-Air:~ d$ time for i in $(cat /tmp/ips.100k);do echo $i > /dev/null;done

real    0m1.629s
user    0m1.154s
sys 0m0.480s
Ds-MacBook-Air:~ d$ time for i in $(cat /tmp/ips.mill);do echo $i > /dev/null;done

real    0m17.567s
user    0m12.414s
sys 0m5.131s
Ds-MacBook-Air:~ d$ time while read i;do echo $i > /dev/null;done < /tmp/ips.100k

real    0m2.148s
user    0m1.493s
sys 0m0.655s
Ds-MacBook-Air:~ d$ time while read i;do echo $i > /dev/null;done < /tmp/ips.mill

real    0m21.536s
user    0m14.915s
sys 0m6.617s

Ds-MacBook-Air:~ d$ tail -5 /tmp/ips.100k /tmp/ips.mill
==> /tmp/ips.100k <==
1.1.134.155
1.1.134.156
1.1.134.157
1.1.134.158
1.1.134.159

==> /tmp/ips.mill <==
1.15.66.59
1.15.66.60
1.15.66.61
1.15.66.62
1.15.66.63

Ds-MacBook-Air:~ d$ wc -l /tmp/ips.100k /tmp/ips.mill
  100000 /tmp/ips.100k
 1000000 /tmp/ips.mill
 1100000 total

关于 for 循环和 while 循环的区别,我没有直接引用的资料,但我记得在《TLDP》Wooldridge文档或其他Bash编程指南中有详细介绍(一些快速的谷歌搜索并没有找到我几年前读到这些内容的确切位置)。


1
如果你只针对bash,那么$(<testfile)$(cat testfile)的更高效替代品。 - Charles Duffy
1
顺便提一下,在 freenode 的 #bash 频道,我们倾向于警告人们不要使用 TLDP 的文档,尤其是 ABS——虽然它不经常完全错误,但它经常展示导致 Bug 的糟糕实践。http://mywiki.wooledge.org/BashGuide 是一个更加精心策划和积极维护的文档。 - Charles Duffy
你重构重定向到 ...; done >/dev/null 会有影响吗? - tripleee
@tripleee 我预计这将在各个方面提高性能,但不会改变for和while之间的差异(假设对for循环进行相同的转换)。 - Charles Duffy
1个回答

5
这里的区别在于,在$(cat testfile)的情况下,您一次性将整个测试文件读入内存并进行字符串拆分,而在while read的情况下,您每次只读取一行。
通常情况下,较少数量的大型读取更为高效。
此外,$(cat testfile)方法会引入错误,它会对文件内容进行字符串拆分(您已经知道),并且会对文件内容进行全局扩展(您可能不知道),也就是说,如果您有一个*,它可以被当前目录中的文件列表替换。

在我看来,磁盘操作的数量在这里并不是很重要。如果是的话,“echo "$(< file)" | while read i”应该接近于“for i in $(< file)”。然而,我只得到了非常微小的改进。虽然“read”是一个内置命令,但我猜它的重复执行会减慢循环速度。 - Socowi
@Socowi,更重要的是read每次只读取一个字节,因此无论是从FIFO还是文件中读取,都会有很多系统调用。 (echo "$(<file)" |方法仍然执行所有这些系统调用,只是从运行echo的bash子shell中的FIFO读取...但您仍然需要支付上下文切换的代价)。 - Charles Duffy

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