我遇到了一个奇怪的问题。我有一个大文件(可能超过1,000,000,000行),它只包含一个代表文件大小的列。它看起来像
55568
9700
7243
9692
63
5508
1679
14072
.....
我想计算每个值的出现次数。我使用两个不同的脚本
注意::下面使用的文件是切割的,只包含10,000行!!!
bob@bob-ruby:~$ cat 1.sh
#!/bin/bash
while read size ; do
set -- $size
((count[$1]++))
done < file-size.txt
bob@bob-ruby:~$
bob@bob-ruby:~$ cat 2.sh
#!/bin/bash
awk '{count[$1]++}' file-size.txt
bob@bob-ruby:~$
我发现1.sh(纯shell脚本)比2.sh(awk-script)慢得多
bob@bob-ruby:~$ time bash 2.sh
real 0m0.045s
user 0m0.012s
sys 0m0.032s
bob@bob-ruby:~$ time bash 1.sh
real 0m0.618s
user 0m0.508s
sys 0m0.112s
bob@bob-ruby:~$
通过'strace'命令,我发现1.sh生成了大量的系统调用,而'2.sh'则少得多,为什么会这样?
这是'awk'在里面做一些'魔术'工作吗?
bob@bob-ruby:~$ strace -c bash 1.sh
% time seconds usecs/call calls errors syscall
------ ----------- ----------- --------- --------- ----------------
38.62 0.042011 1 30320 rt_sigprocmask
29.97 0.032597 2 20212 _llseek
15.33 0.016674 2 10115 read
12.57 0.013675 1 10106 10106 ioctl
(cut)
bob@bob-ruby:~$ strace -c bash 2.sh
% time seconds usecs/call calls errors syscall
------ ----------- ----------- --------- --------- ----------------
95.52 0.008000 4000 2 1 waitpid
3.20 0.000268 21 13 5 access
1.28 0.000107 5 21 fstat64
0.00 0.000000 0 9 read
答案 0 :(得分:3)
最大的区别是,while
循环版本需要一次读取一行文件,awk
读取输入 整个文件< / strike>并在内存中解析它。你很幸运read
是内置的,或效率会大大降低。 shell脚本的通常情况是每个while
循环迭代产生多个子进程来处理一行。它们可能要慢得多 - 考虑使用以下方法将行解析为字段:
while
read line
do
field1=`echo $line | cut -f 1 -d '|'`
field2=`echo $line | cut -f 2 -d '|'`
...
done
我继承了一个以这种方式处理数据库输出的shell脚本。当我用一小段awk
将一个多小时的批处理过程变成大约20分钟时,我的经理感到很惊讶。
修改强>
我挖了awk source code,因为我对这个很好奇。看起来这只是简单调用getc
后隐藏的标准IO缓冲的简单用法。 C标准库在输入流上实现有效的缓冲。我使用以下非常简单的shell脚本运行dtruss
#!/bin/zsh
while
read line
do
echo "$line"
done < blah.c
输入 blah.c 是一个包含7219行的191349字节C文件。
dtruss
输出包含4266次调用read
,缓冲区大小为1字节,用于shell脚本。看来zsh
根本没有缓冲输入。我使用bash
进行了相同的测试,它包含的read
调用序列完全相同。另一个重要的注意事项是zsh
生成了6074个系统调用,bash
生成了6604个系统调用。
等效的awk '{print}' blah.c
命令显示56个调用read_nocancel
,缓冲区大小为4096.它总共有160个系统调用。
考虑这个问题最简单的方法是awk
是一个解析文本生活的程序,shell关注进程管理,管道连接,以及通常为用户交互运行程序。您应该使用适当的工具来完成手头的工作。如果您正在处理来自大型文件的数据,请避开通用shell命令 - 这不是shell的意图,它不会非常有效地执行。如果你正在编写背靠背执行shell实用程序的脚本,那么你不希望在perl或python中编写它,因为处理子进程的退出状态和它们之间的流水线操作会很痛苦。
答案 1 :(得分:3)
Chet Ramey的答案(chet.ramey@case.edu)
12月21日下午9:59,boblin写道:
嗨,chet:
I had meet a strange problem . I have a large file (maybe more than
10,000行),其中只包含一个代表大小的列 的文件。它看起来像
55568 9700 7243 9692 63 5508 1679 14072 .....
我想计算每个值的出现次数。我使用两种不同的方法
bob@bob-ruby:~$ cat 1.sh #!/bin/bash while read size ; do set -- $size ((count[$1]++)) done < file-size.txt bob@bob-ruby:~$
这实际上是一种效率低下的方法,但并非如此 做出巨大的改变。没有必要仅仅为化妆品使用`set' 原因。你可以做到
读取大小;做 ((计数[$大小] ++)) 完成&lt;文件size.txt
bob@bob-ruby:~$ cat 2.sh #!/bin/bash awk '{count[$1]++}' file-size.txt bob@bob-ruby:~$
我发现1.sh(纯shell脚本)比2.sh(awk-script)慢得多
bob@bob-ruby:~$ time bash 2.sh real 0m0.045s user 0m0.012s sys 0m0.032s bob@bob-ruby:~$ time bash 1.sh real 0m0.618s user 0m0.508s sys 0m0.112s bob@bob-ruby:~$
通过strace命令,我发现1.sh生成了很多系统调用,而 “2.sh”要少得多,为什么会这样?
因为你没跟踪awk。你跟踪了bash调用并等待 AWK。这就是为什么`waitpid'占据了执行时间的原因。
awk是否在里面做任何'魔术'工作?
awk对其操作的限制要少得多,如下所述。
bob@bob-ruby:~$ strace -c bash 1.sh % time seconds usecs/call calls errors syscall ------ ----------- ----------- --------- --------- ---------------- 38.62 0.042011 1 30320 rt_sigprocmask 29.97 0.032597 2 20212 _llseek 15.33 0.016674 2 10115 read 12.57 0.013675 1 10106 10106 ioctl
bash调用sigprocmask有一个问题,因为它会调用 setjmp以setjmp保存和恢复信号掩码的方式。一世 做了一些关于信号和陷阱的工作,这将允许下一个版本 避免恢复信号掩码。
lseeks和read必须留下来。我想awk可以读取尽可能多的数据 它想要进入内部缓冲区并从内存中处理它。外壳是 需要将文件偏移重置为每次之后消耗的内容 读取,所以它调用的程序可以获得预期的标准输入 - 它是 读取内置调用之间不允许提前读取。这意味着 shell必须测试它正在读取的文件描述符 每次读取内置运行时都能够查找 - 终端和管道 无法在数据流中向后搜索,因此shell必须读取一个 那些人的角色。 shell的内置读取功能很少 缓冲,所以即使对于shell可以向后搜索的常规文件, 它必须调用lseek来调整内置读取之前的文件指针 返回一条线。这也增加了所需的read(2)调用次数: 在某些情况下,shell会多次从文件中读取相同的数据, 每次调用读取时至少需要一次read(2)调用 内置
ioctl是告诉输入fd是否附加到a 终奌站;除了无缓冲读取之外,还有几个选项 使用终端时可用。每次通话至少有一个lseek 读内置,判断输入fd是否为管道。
这说明你在strace输出中列出的系统调用。
The lyf so short, the craft so long to lerne.'' - Chaucer
Ars longa,vita brevis'' - 希波克拉底
Chet Ramey,ITS,CWRU chet@case.edu http://cnswww.cns.cwru.edu/~chet/