4

我遇到了一个奇怪的问题。我有一个大文件(可能超过 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产生了很多syscall,而'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
4

2 回答 2

4

Chet Ramey 的回答(chet.ramey@case.edu)

2012 年 12 月 21 日晚上 9:59,boblin 写道:

嗨,切特:

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”。你可以做

同时读取大小;do ((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-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 产生了很多 syscall,而 '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 和 reads 必须保留。我想 awk 可以将尽可能多的数据读取到内部缓冲区中并从内存中处理它。shell 需要在每次读取后将文件偏移量重置回它所使用的值,因此它调用的程序可以获得预期的标准输入——它不允许在 read 内置调用之间提前读取。这意味着 shell 必须测试它正在读取的文件描述符,以便每次运行 read 内置函数时都能够进行查找——终端和管道不能在数据流中向后查找,因此 shell 必须读取一个字符从那些时间。shell 的 read 内置函数做了一些最小的缓冲,所以即使对于 shell 可以向后搜索的常规文件,它也必须在 read 内置函数返回一行之前调用 lseek 来调整文件指针。

ioctl 用于判断输入 fd 是否连接到终端;除了无缓冲读取之外,还有几个选项仅在使用终端时可用。每次调用 read 内置函数至少有一个 lseek ,以判断输入 fd 是否是管道。

这说明了您在 strace 输出中列出的系统调用。

切特

The lyf so short, the craft so long to lerne.'' - Chaucer Ars longa, vita brevis'' - Hippocrates Chet Ramey, ITS, CWRU chet@case.edu http://cnswww.cns.cwru.edu/~chet/

于 2013-01-01T10:06:06.223 回答
3

最大的不同是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.

编辑
我挖掘了awk 源代码,因为我对此感到好奇。看起来这是标准 IO 缓冲的简单用法,隐藏在对getc. C 标准库在输入流上实现了有效的缓冲。我dtruss使用以下非常简单的 shell 脚本运行

#!/bin/zsh
while
    read line
do
    echo "$line"
done < blah.c

输入blah.c是一个 191349 字节的 C 文件,包含 7219 行。

输出包含 4266次dtruss调用,readshell 脚本的缓冲区大小为 1 字节。看起来zsh根本没有缓冲它的输入。我使用它进行了相同的测试bash,它包含完全相同的read调用序列。另一个重要的注意事项是zsh生成了 6074 个系统调用和bash生成了 6604 个系统调用。

等效awk '{print}' blah.c命令显示了 56 次调用,read_nocancel缓冲区大小为 4096。它总共有 160 次系统调用。


考虑这一点的最简单方法是,它awk是一个以解析文本为生的程序,而 shell 则关注进程管理、管道连接以及通常为用户交互运行的程序。您应该为手头的工作使用适当的工具。如果您正在处理来自大文件的数据,请避开通用的 shell 命令——这不是 shell 的本意,而且它不会非常有效地执行此操作。如果您正在编写背靠背执行 shell 实用程序的脚本,那么您不希望用 perl 或 python 编写它,因为处理子进程的退出状态和它们之间的流水线会很痛苦。

于 2012-12-27T03:49:09.660 回答