【问题标题】:performance issue about 'while + read' .vs. AWK关于“while + read”.vs 的性能问题。 AWK
【发布时间】:2012-12-12 12:10:57
【问题描述】:

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

【问题讨论】:

  • 为什么这很奇怪? Shell 脚本并不以其性能而闻名(它们很慢)。
  • 纯粹出于好奇,cat file | sort | uniq -c 需要什么时间?
  • 我读过 ABS 指南,它告诉我们尽可能少使用管道,
  • 这些脚本甚至不会产生任何要检查的输出。可能是 awk 只是优化了计算
  • @favoretti,由于 OP 非常关心管道操作员,您可以通过消除 cat 的无用使用并只说 sort -n | uniq -c 来节省两者之一

标签: performance bash awk while-loop


【解决方案1】:

最大的区别是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 source code,因为我对这个很好奇。看起来这是隐藏在对 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 编写它,因为处理子进程的退出状态和它们之间的流水线会很痛苦。

【讨论】:

  • 文件很大(6GB),物理内存为8GB。当我运行 AWK 脚本时,它似乎只消耗不多的 Mem (
  • count 数组添加新元素时,您只会增加内存。祝大家好运。
  • 但@D.Shawley 说:'awk 读取 整个 文件并在内存中解析它。' .我知道 while-loop 非常慢,因为它会导致太多“读取”系统调用。但是 AWK 怎么能只进行 9 次“读取”调用呢?
【解决方案2】:

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” 原因。你可以这样做

同时读取大小;做 ((计数[$大小]++)) 完成

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 调用和等待 哦。这就是为什么 `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 可以读取尽可能多的数据 它想要进入一个内部缓冲区并从内存中处理它。外壳是 需要将文件偏移量重置为每次使用后所消耗的值 读,所以它调用的程序可以获得预期的标准输入——它是 不允许在 read 内置调用之间预读。这意味着 shell 必须测试它正在读取的文件描述符 每次运行 read 内置函数时都能够查找——终端和管道 不能在数据流中向后搜索,所以shell必须读取一个 一次从那些字符。 shell 的 read 内置函数做了一些最小的事情 缓冲,所以即使对于shell可以向后搜索的常规文件, 它必须在 read 内置函数之前调用 lseek 来调整文件指针 返回一行。这也增加了所需的 read(2) 调用次数: 在某些情况下,shell 会多次从文件中读取相同的数据, 并且每次调用 read 至少需要一个 read(2) 调用 内置。

ioctl 是用来判断输入 fd 是否附加到一个 终端;除了无缓冲读取之外,还有几个选项仅 使用终端时可用。每次调用至少有一个 lseek 到 read 内置,判断输入 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/

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2011-09-01
    • 1970-01-01
    • 2011-05-28
    • 1970-01-01
    • 2022-09-24
    • 2017-10-23
    • 1970-01-01
    相关资源
    最近更新 更多