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



我遇到了一个奇怪的问题。我有一个大文件(可能超过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编写它,因为处理子流程的退出状态和它们之间的流水线操作会很痛苦。

最新更新