关于'while + read'.vs的性能问题。 AWK

时间:2012-12-27 03:24:09

标签: performance bash awk while-loop

我遇到了一个奇怪的问题。我有一个大文件(可能超过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

2 个答案:

答案 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/