4

私は奇妙な問題に遭遇しました。file のサイズを表す単一の列のみを含む大きなファイル (おそらく 1,000,000,000 行以上) があります。のように見えます

55568
9700
7243
9692
63
5508
1679
14072
.....

そして、各値の出現回数を数えたいと思います。私は2つの異なるスクリプトを使用しています

注:: 以下で使用されるファイルは切り取られており、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 (純粋なシェル スクリプト) は 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@case.edu)

12/21/12 9:59 PMに、ボブリンは次のように書いています。

こんにちは、チェット:

I had meet a strange problem . I have a large file (maybe more than

10,000行)これには、ファイルのサイズを表す1つの列のみが含まれます。のように見えます

55568
9700
7243
9692
63
5508
1679
14072
.....

そして、それぞれの値の出現を数えたいと思います。私は2つの異なる方法を使用します

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(純粋なシェルスクリプト)は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

sigprocmaskを呼び出すbashには、setjmpがシグナルマスクを保存および復元するような方法でsetjmpを呼び出すため、多くの問題があります。次のバージョンでシグナルマスクの復元を回避できるようにするシグナルとトラップについて、いくつかの作業を行いました。

lseeksとreadsはとどまらなければなりません。awkは必要なだけのデータを内部バッファーに読み込んで、メモリから処理できると思います。シェルは、各読み取り後に消費したファイルオフセットにリセットする必要があるため、シェルが呼び出すプログラムは、目的の標準入力を取得できます。つまり、組み込みの読み取り呼び出しの間に先読みすることはできません。つまり、シェルは、読み取りビルトインが実行されるたびにシークする機能について、読み取り元のファイル記述子をテストする必要があります。ターミナルとパイプはデータストリーム内で逆方向にシークできないため、シェルはで1文字を読み取る必要があります。それらからの時間。シェルのreadビルトインは最小限のバッファリングを行うため、シェルが逆方向にシークできる通常のファイルの場合でも、readビルトインが行を返す前にlseekを呼び出してファイルポインタを調整する必要があります。

ioctlは、入力fdが端末に接続されているかどうかを通知します。バッファなしの読み取りに加えて、いくつかのオプションは端末を使用している場合にのみ使用できます。入力fdがパイプであるかどうかを判断するために、readビルトインへの呼び出しごとに少なくとも1つのlseekがあります。

これは、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一度に 1 行ずつファイルを読み取るためにループ バージョンが必要であり、ファイル全体の入力awkを読み取ってメモリ内で解析することです。あなたはそれがビルトインである幸運です。そうしないと、かなり効率が悪くなります。シェル スクリプトの通常のケースは、各ループの反復が複数の子プロセスを生成して行を処理することです。それらはかなり遅くなる可能性があります-次を使用して行をフィールドに解析することを検討してください: readwhile

while
  read line
do
  field1=`echo $line | cut -f 1 -d '|'`
  field2=`echo $line | cut -f 2 -d '|'`
  ...
done

この方法でデータベース出力を処理するシェル スクリプトを継承しました。単純なawk.

編集これに興味があったので、 awk ソースコード
を掘り下げました。これは、 への単純な呼び出しの背後に隠された標準 IO バッファリングの単純な使用法のようです。C 標準ライブラリは、入力ストリームに効率的なバッファリングを実装します。次の非常に単純なシェルスクリプトを使用して実行しましたgetcdtruss

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

入力blah.cは、7219 行を含む 191349 バイトの C ファイルです。

出力には、シェル スクリプトのバッファ サイズが 1 バイトの への4266dtruss回の呼び出しが含まれていました。入力をまったくバッファリングしていないreadようです。zshを使用して同じテストをbash行いましたが、まったく同じ一連のread呼び出しが含まれていました。もう 1 つの重要な注意事項は、zsh生成された 6074 システム コールとbash生成された 6604 システム コールです。

同等のawk '{print}' blah.cコマンドは、4096 のバッファ サイズで 56 の呼び出しを示しread_nocancelました。合計 160 のシステム コールがありました。


これについて考える最も簡単な方法はawk、生計を立てるためにテキストを解析するプログラムであり、シェルはプロセス管理、パイプラインの接続、および一般的にユーザーのためにインタラクティブにプログラムを実行することに関係しているということです。手元の作業に適したツールを使用する必要があります。大きなファイルのデータを処理する場合は、汎用のシェル コマンドを使用しないようにしてください。これは、シェルが行うことを意図したものではなく、非常に効率的ではありません。シェル ユーティリティを連続して実行するスクリプトを作成している場合、サブプロセスの終了ステータスの処理とサブプロセス間のパイプライン処理が面倒になるため、perl や Python でスクリプトを作成することは望ましくありません。

于 2012-12-27T03:49:09.660 に答える