我遇到了一个奇怪的问题。我有一个大文件(可能超过1000000000行(,它只包含一列,表示文件的大小。看起来像
55568
9700
7243
9692
63
5508
1679
14072
.....
我想计算每个值的出现次数。我使用两种不同的脚本
注意:下面使用的文件是剪切的,仅包含10000行!!!
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脚本(慢得多
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
Chet Ramey的回答(chet.ramey@case.edu)
2012年12月21日晚上9:59,博布林写道:
嗨,切特:
I had meet a strange problem . I have a large file (maybe more than
10000行(,其中仅包含表示大小的单个列文件的。看起来像
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"原因。你可以做
同时读取大小;做((count[$size]++((done<file-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脚本(慢得多
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对其运营的限制要少得多,如下所述。
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保存并恢复信号掩码。我在信号和陷阱方面做了一些工作,这将允许下一个版本避免恢复信号掩码。
lseeks和reads必须留下来。我想awk可以读取与它想要进入一个内部缓冲区并从内存中处理它。外壳是需要将文件偏移量重置回每次之后的消耗量读取,这样它调用的程序就可以获得预期的标准输入——不允许在读取内置调用之间提前读取。这意味着shell必须测试从中读取的文件描述符每次运行read内建时查找的能力--终端和管道无法在数据流中向后搜索,因此shell必须读取一个一次一个角色。shell的read内建做了一些最小化的操作缓冲,因此即使对于shell可以向后查找的常规文件,它必须调用lseek来调整文件指针,然后才能读取内置文件返回一行。这也增加了所需的read(2(调用次数:在某些情况下,shell从文件中多次读取相同的数据,并且每次调用读取至少需要一个读取(2(调用内置。
ioctl用于判断输入fd是否附加到航空站除了无缓冲读取之外,只有几个选项在使用终端时可用。每次呼叫至少有一个lseek以判断输入fd是否为管道。
这说明了strace输出中列出的系统调用。
Chet
The lyf so short, the craft so long to lerne.'' - Chaucer
Ars longa,vita brevis’-希波克拉底Chet Ramey,ITS,CWRUchet@case.eduhttp://cnswww.cns.cwru.edu/~chet/
最大的区别是while
循环版本需要一次读取一行文件,而awk
读取整个文件的输入并在内存中解析它。幸运的是,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源代码,因为我对这个很好奇。看起来这是隐藏在对getc
的简单调用后面的标准IO缓冲区的简单用法。C标准库在输入流上实现了高效的缓冲。我使用以下非常简单的shell脚本运行dtruss
#!/bin/zsh
while
read line
do
echo "$line"
done < blah.c
输入blah.c是一个191349字节的c文件,包含7219行。
dtruss
输出包含对read
的4266次调用,shell脚本的缓冲区大小为1字节。看起来zsh
根本没有缓冲它的输入。我使用bash
做了同样的测试,它包含完全相同的read
调用序列。另一个重要的注意事项是zsh
生成了6074个系统调用,bash
生成了6604个系统呼叫。
等效的awk '{print}' blah.c
命令显示了对read_nocancel
的56次调用,缓冲区大小为4096。它总共有160个系统调用。
考虑这一点最简单的方法是,awk
是一个以解析文本为生的程序,shell关心流程管理、管道连接,以及通常为用户交互运行程序。你应该为手头的工作使用合适的工具。如果您正在处理来自大文件的数据,请避开通用shell命令——这不是shell的目的,它也不会非常有效地执行。如果您正在编写背靠背执行shell实用程序的脚本,那么您就不想用perl或python编写它,因为处理子流程的退出状态和它们之间的流水线操作会很痛苦。