ファイルをメモリに読み込んで2回計算するよりも、2回高速にファイルを繰り返し処理するのはなぜですか?


26

私は以下を比較しています

tail -n 1000000 stdout.log | grep -c '"success": true'
tail -n 1000000 stdout.log | grep -c '"success": false'

次の

log=$(tail -n 1000000 stdout.log)
echo "$log" | grep -c '"success": true'
echo "$log" | grep -c '"success": false'

驚いたことに、2番目は最初の2倍近くかかります。それはもっと速いはずですよね?


2番目の解決策、ファイルの内容が3回読み取られ、最初の例では2回しか読み取られないためでしょうか?
ローランC. 14年

4
少なくとも2番目の例で$( command substitution )は、あなたはストリーミングされません。残りはすべてパイプを介して同時に発生しますが、2番目の例ではlog=、完了するまで待つ必要があります。<< HERE \ n $ {log = $(command)} \ nHEREで試してください-何が得られるか確認してください。
mikeserv 14年

非常に大きなファイル、メモリに制約のあるマシン、またはより多くのアイテムをgrep使用teeする場合、ファイルの読み込みが確実に1回だけになるように使用すると速度が向上する場合があります。cat stdout.log | tee >/dev/null >(grep -c 'true'>true.cnt) >(grep -c 'false'>false.cnt); cat true.cnt; cat false.cnt
マット14年

@LaurentC。、いいえ、2番目の例では1回だけ読み取られます。tailへの呼び出しは1つだけです。
psusi

次に、これらをtail -n 10000 | fgrep -c '"success": true'falseと比較します。
小次郎14年

回答:


11

一方では、最初のメソッドはtail2回呼び出すため、これを1回だけ行う2番目のメソッドよりも多くの作業を行う必要があります。一方、2番目のメソッドは、データをシェルにコピーしてからバックアウトする必要があるため、にtail直接パイプされる最初のバージョンよりも多くの作業を行う必要がありgrepます。第一の方法は、マルチプロセッサ・マシン上の余分な利点を有する:grepと並列に動作することができるtail第二の方法は、厳密に最初に、シリアル化され、一方、tailその後、grep

したがって、一方が他方よりも高速である必要がある明確な理由はありません。

何が起こっているのかを知りたい場合は、シェルが呼び出すシステムを調べてください。別のシェルでも試してください。

strace -t -f -o 1.strace sh -c '
  tail -n 1000000 stdout.log | grep "\"success\": true" | wc -l;
  tail -n 1000000 stdout.log | grep "\"success\": false" | wc -l'

strace -t -f -o 2-bash.strace bash -c '
  log=$(tail -n 1000000 stdout.log);
  echo "$log" | grep "\"success\": true" | wc -l;
  echo "$log" | grep "\"success\": true" | wc -l'

strace -t -f -o 2-zsh.strace zsh -c '
  log=$(tail -n 1000000 stdout.log);
  echo "$log" | grep "\"success\": true" | wc -l;
  echo "$log" | grep "\"success\": true" | wc -l'

方法1では、主な段階は次のとおりです。

  1. tail 読み取り、開始点を探します。
  2. tail4096バイトのチャンクをgrep書き込み、生成される速度で読み込みます。
  3. 2番目の検索文字列に対して前の手順を繰り返します。

方法2では、主な段階は次のとおりです。

  1. tail 読み取り、開始点を探します。
  2. tail bashが一度に128バイトを読み取る4096バイトのチャンクを書き込み、zshが一度に4096バイトを読み取ります。
  3. Bashまたはzshは、4096バイトのチャンクを書き込みgrepますが、生成されるのと同じ速さで読み取ります。
  4. 2番目の検索文字列に対して前の手順を繰り返します。

コマンド置換の出力を読み取るときのBashの128バイトのチャンクは、大幅に速度を低下させます。私にとって、zshは方法1とほぼ同じ速さで出てきます。燃費は、CPUのタイプと数、スケジューラー構成、関連するツールのバージョン、およびデータのサイズによって異なる場合があります。


4kフィギュアのページサイズは依存していますか?つまり、tailとzshはどちらもシステムコールをmmapしていますか?(おそらくそれは間違った用語ですが、そうではないことを願っていますが...)bashはどう違うのですか?
mikeserv 14年

これはジルのスポットです!zshを使用すると、2番目の方法はマシン上でわずかに高速になります。
プネヘヘ14年

素晴らしい仕事Gilles、tks。
X天

@mikeservこれらのプログラムがどのようにサイズを選択するかを知るために、ソースを見ていません。4096を表示する最も可能性の高い理由は、組み込み定数またはst_blksizeパイプの値であり、これはこのマシンでは4096です(これがMMUページサイズであるためかどうかはわかりません)。Bashの128は組み込み定数である必要があります。
ジル 'SO-悪であるのをやめる' 14年

@Gilles、思慮深い返事をありがとう。私は最近、ページサイズに興味がありました。
mikeserv 14年

26

次のテストを実行しましたが、私のシステムでは、2番目のスクリプトの結果の差は約100倍長くなります。

私のファイルはと呼ばれるstrace出力です bigfile

$ wc -l bigfile.log 
1617000 bigfile.log

スクリプト

xtian@clafujiu:~/tmp$ cat p1.sh
tail -n 1000000 bigfile.log | grep '"success": true' | wc -l
tail -n 1000000 bigfile.log | grep '"success": false' | wc -l

xtian@clafujiu:~/tmp$ cat p2.sh
log=$(tail -n 1000000 bigfile.log)
echo "$log" | grep '"success": true' | wc -l
echo "$log" | grep '"success": true' | wc -l

実際にはgrepに一致するものがないので、最後のパイプまで何も書き込まれません wc -l

タイミングは次のとおりです。

xtian@clafujiu:~/tmp$ time bash p1.sh
0
0

real    0m0.381s
user    0m0.248s
sys 0m0.280s
xtian@clafujiu:~/tmp$ time bash p2.sh
0
0

real    0m46.060s
user    0m43.903s
sys 0m2.176s

そこで、straceコマンドを使用して2つのスクリプトを再度実行しました

strace -cfo p1.strace bash p1.sh
strace -cfo p2.strace bash p2.sh

トレースの結果は次のとおりです。

$ cat p1.strace 
% time     seconds  usecs/call     calls    errors syscall
------ ----------- ----------- --------- --------- ----------------
 97.24    0.508109       63514         8         2 waitpid
  1.61    0.008388           0     84569           read
  1.08    0.005659           0     42448           write
  0.06    0.000328           0     21233           _llseek
  0.00    0.000024           0       204       146 stat64
  0.00    0.000017           0       137           fstat64
  0.00    0.000000           0       283       149 open
  0.00    0.000000           0       180         8 close
...
  0.00    0.000000           0       162           mmap2
  0.00    0.000000           0        29           getuid32
  0.00    0.000000           0        29           getgid32
  0.00    0.000000           0        29           geteuid32
  0.00    0.000000           0        29           getegid32
  0.00    0.000000           0         3         1 fcntl64
  0.00    0.000000           0         7           set_thread_area
------ ----------- ----------- --------- --------- ----------------
100.00    0.522525                149618       332 total

そして、p2.strace

$ cat p2.strace 
% time     seconds  usecs/call     calls    errors syscall
------ ----------- ----------- --------- --------- ----------------
 75.27    1.336886      133689        10         3 waitpid
 13.36    0.237266          11     21231           write
  4.65    0.082527        1115        74           brk
  2.48    0.044000        7333         6           execve
  2.31    0.040998        5857         7           clone
  1.91    0.033965           0    705681           read
  0.02    0.000376           0     10619           _llseek
  0.00    0.000000           0       248       132 open
...
  0.00    0.000000           0       141           mmap2
  0.00    0.000000           0       176       126 stat64
  0.00    0.000000           0       118           fstat64
  0.00    0.000000           0        25           getuid32
  0.00    0.000000           0        25           getgid32
  0.00    0.000000           0        25           geteuid32
  0.00    0.000000           0        25           getegid32
  0.00    0.000000           0         3         1 fcntl64
  0.00    0.000000           0         6           set_thread_area
------ ----------- ----------- --------- --------- ----------------
100.00    1.776018                738827       293 total

分析

当然のことながら、どちらの場合もほとんどの時間はプロセスの完了を待つことに費やされますが、p2はp1より2.63倍長く待機します。

では、を忘れて、列をwaitpid無視して、%両方のトレースの秒列を見てください。

最大の時間 p1は、おそらく大きなファイルを読み込むために、おそらく読み取りにほとんどの時間を費やしますが、p2はp1の28.82倍の読み取りに費やします。- bashこのような大きなファイルを変数に読み込むことを期待しておらず、おそらく一度にバッファーを読み込み、行に分割してから別のものを取得します。

読み込みカウント p2は705k対p1の84kであり、各読み込みではコンテキストの切り替えが必要であり、カーネル空間に切り替えて再度出力します。読み取りおよびコンテキスト切り替えの数のほぼ10倍。

書き込み時間 p2は、p1よりも書き込みに41.93倍長くかかります

書き込みカウント p1は、p2よりも多くの書き込みを行いますが、42k対21kですが、はるかに高速です。

おそらく、末尾の書き込みバッファーとは対照的にecho、行の数が多いためですgrep

さらに、p2は読み取りよりも書き込みに多くの時間を費やします。p1は逆です。

その他の要因brkシステムコールの数を見てください。p2は、読み取りよりも2.42倍長い中断を費やします。p1では(登録すらしません)。brk最初に十分な容量が割り当てられなかったためにプログラムがアドレス空間を拡張する必要がある場合、これはおそらくbashがそのファイルを変数に読み込む必要があり、その大きさを期待していないためです。ファイルが大きすぎる場合でも、それは動作しません。

tailこれはおそらく非常に効率的なファイルリーダーです。これは、ファイルのmemmapを実行して改行をスキャンし、カーネルがI / Oを最適化できるように設計されているためです。bashは、読み取りと書き込みに費やした時間の両方であまり良くありません。

p2は44ミリ秒と41ミリ秒を費やしてcloneおりexecv、p1の測定可能な量ではありません。おそらくbashを読み取って、末尾から変数を作成します。

最後に、合計 p1は p2 740k(4.93倍)に対して、約15万のシステムコールを実行します。

waitpidを削除すると、p1はシステムコールの実行に0.014416秒、p2は0.439132秒(30倍長い)かかります。

したがって、p2はユーザー空間でほとんどの時間を費やしてシステムコールの完了とカーネルのメモリ再編成を待機する以外に何もしませんが、p1はより多くの書き込みを実行しますが、より効率的でシステム負荷が大幅に少なくなり、したがって高速です。

結論

私は、bashスクリプトを記述するときにメモリを介したコーディングについて心配することは決してありません。

tailおそらくmemory mapsファイルであるため、読み取りが効率的になり、カーネルがI / Oを最適化できるようになります。

あなたの問題を最適化するためのより良い方法は、最初にあるかもしれないgrep「『成功』:」の行と、その後truesとfalsesをカウントし、grepカウント再び回避オプションがあるwc -lに至るまで、まださらに良く、または、パイプ尾をawkしてtruesをカウントし、同時に偽。p2は時間がかかるだけでなく、メモリがbrksでシャッフルされている間にシステムに負荷を追加します。


2
TL; DR:malloc(); 必要な大きさを$ logに伝え、再割り当てを行わずに1つのopですばやく書き込むことができれば、おそらく同じくらい速いでしょう。
クリスK 14年

5

実際、最初の解決策もファイルをメモリに読み込みます!これはキャッシングと呼ばれ、オペレーティングシステムによって自動的に行われます。

そして、既に正しくによって説明mikeserv第一溶液のexectutesをgrep しながら、第二の溶液は、その実行のに対し、ファイルが読み込まれている後に、ファイルがで読み取りましたtail

したがって、最初のソリューションはさまざまな最適化により高速です。しかし、これは常に真実である必要はありません。OSがキャッシュしないと決定した非常に大きなファイルの場合、2番目のソリューションはより高速になる可能性があります。ただし、メモリに収まらない大きなファイルの場合、2番目のソリューションはまったく機能しません。


3

主な違いは非常に単純でecho遅いと思います。このことを考慮:

$ time (tail -n 1000000 foo | grep 'true' | wc -l; 
        tail -n 1000000 foo | grep 'false' | wc -l;)
666666
333333

real    0m0.999s
user    0m1.056s
sys     0m0.136s

$ time (log=$(tail -n 1000000 foo); echo "$log" | grep 'true' | wc -l; 
                                    echo "$log" | grep 'false' | wc -l)
666666
333333

real    0m4.132s
user    0m3.876s
sys     0m0.468s

$ time (tail -n 1000000 foo > bb;  grep 'true' bb | wc -l; 
                                   grep 'false' bb | wc -l)
666666
333333

real    0m0.568s
user    0m0.512s
sys     0m0.092s

上記のように、時間のかかるステップはデータの印刷です。単に新しいファイルにリダイレクトし、それを介してgrepを実行すると、ファイルを1回だけ読み取る場合にはるかに高速になります。


そして、要求どおり、here文字列を使用します。

 $ time (log=$(tail -n 1000000 foo); grep 'true' <<< $log | wc -l; 
                                     grep 'false' <<< $log | wc -l  )
1
1

real    0m7.574s
user    0m7.092s
sys     0m0.516s

おそらくhere文字列がすべてのデータを1つの長い行に連結しているため、これはさらに遅くなりますgrep

$ tail -n 1000000 foo | (time grep -c 'true')
666666

real    0m0.500s
user    0m0.472s
sys     0m0.000s

$ tail -n 1000000 foo | perl -pe 's/\n/ /' | (time grep -c 'true')
1

real    0m1.053s
user    0m0.048s
sys     0m0.068s

分割が発生しないように変数が引用されている場合、事態は少し速くなります。

 $ time (log=$(tail -n 1000000 foo); grep 'true' <<< "$log" | wc -l; 
                                     grep 'false' <<< "$log" | wc -l  )
666666
333333

real    0m6.545s
user    0m6.060s
sys     0m0.548s

ただし、レート制限ステップでデータが印刷されるため、依然として低速です。


試し<<<てみて、それが違いを生むかどうかを確認するのは面白いでしょう。
グレアム14年

3

私もこれを試しました...最初に、ファイルを作成しました:

printf '"success": "true"
        "success": "true"
        "success": "false"
        %.0b' `seq 1 500000` >|/tmp/log

上記を自分で実行する場合/tmp/logは、"success": "true"行と"success": "false"行の比率が2:1の150万行を作成する必要があります。

次にしたことは、いくつかのテストを実行することでした。私は、プロキシを介してすべてのテストを実行しましたshのでtime、したがって、全体の仕事のための単一の結果を示すことができた-ただ一つのプロセスを監視する必要があります。

これは、2番目のファイル記述子を追加しますが、tee,理由を説明できると思いますが、最速のようです。

    time sh <<-\CMD
        . <<HD /dev/stdin | grep '"success": "true"' | wc -l
            tail -n 1000000 /tmp/log | { tee /dev/fd/3 |\
                grep '"success": "false"' |\
                    wc -l 1>&2 & } 3>&1 &
        HD
    CMD
666666
333334
sh <<<''  0.11s user 0.08s system 84% cpu 0.224 total

あなたの最初は次のとおりです。

    time sh <<\CMD
        tail -n 1000000 /tmp/log | grep '"success": "true"' | wc -l
        tail -n 1000000 /tmp/log | grep '"success": "false"' | wc -l
    CMD

666666
333334
sh <<<''  0.31s user 0.17s system 148% cpu 0.323 total

そしてあなたの2番目:

    time sh <<\CMD
        log=$(tail -n 1000000 /tmp/log)
        echo "$log" | grep '"success": "true"' | wc -l
        echo "$log" | grep '"success": "false"' | wc -l
    CMD
666666
333334
sh <<<''  2.12s user 0.46s system 108% cpu 2.381 total

私のテストでは、変数に読み込んだときのように速度が3倍以上異なることがわかります。

その一部は、シェル変数が読み取られているときに、シェルによって分割されて処理される必要があることだと思います-ファイルではありません。

Aは、here-document他の一方で、すべての意図や目的のために、あるfile-file descriptor,とにかく。そして、ご存じのとおり、Unixはファイルを処理ます。

何についての私には最も興味深いのはhere-docs、あなたが自分の操作することができるということですfile-descriptorsストレートとして-を|pipe-と、それらを実行します。これは、目的の|pipe場所を指定する際にもう少し自由にできるため、非常に便利です。

最初の人が食べて、2番目の人が読むものが何もないので、私はしなければteeなりませんでした。しかし、私はそれを引き継いで再び引き継いだので、大した問題ではありませんでした。他の人が推奨する数を使用する場合:tailgrephere-doc |pipe|piped/dev/fd/3>&1 stdout,grep -c

    time sh <<-\CMD
        . <<HD /dev/stdin | grep -c '"success": "true"'
            tail -n 1000000 /tmp/log | { tee /dev/fd/3 |\
                grep -c '"success": "false"' 1>&2 & } 3>&1 &
        HD
    CMD
666666
333334
sh <<<''  0.07s user 0.04s system 62% cpu 0.175 total

さらに高速です。

しかし、私はせずに、それを実行したときに、私ができない成功した背景に完全に並行して、それらを実行するための最初のプロセス。ここでは、完全にバックグラウンド化されていません。. sourcingheredoc

    time sh <<\CMD
        tail -n 1000000 /tmp/log | { tee /dev/fd/3 |\
            grep -c '"success": "true"' 1>&2 & } 3>&1 |\
                grep -c '"success": "false"'
    CMD
666666
333334
sh <<<''  0.10s user 0.08s system 109% cpu 0.165 total

しかし、私が追加するとき &:

    time sh <<\CMD
        tail -n 1000000 /tmp/log | { tee /dev/fd/3 |\
            grep -c '"success": "true"' 1>&2 & } 3>&1 & |\
                grep -c '"success": "false"'
    CMD
sh: line 2: syntax error near unexpected token `|'

それでも、少なくとも私にとっては、その差はわずか数百分の1秒であるように見えるので、あなたが思うようにそれを受け入れてください。

とにかく、それがより速く実行される理由teeは、両方grepsを一度に実行するだけでtail. teeファイルを複製し、grepすべてをインストリームで2番目のプロセスに分割するためです-すべてが最初から最後まで一度に実行されるため、すべてがほぼ同時に終了します。

最初の例に戻ります:

    tail | grep | wc #wait til finished
    tail | grep | wc #now we're done

そしてあなたの2番目:

    var=$( tail ) ; #wait til finished
    echo | grep | wc #wait til finished
    echo | grep | wc #now we're done

しかし、入力を分割し、プロセスを同時に実行する場合:

          3>&1  | grep #now we're done
              /        
    tail | tee  #both process together
              \  
          >&1   | grep #now we're done

1
+1ですが、最後のテストは構文エラーで死亡しました。そこで時間は正しいとは思いません:)
terdon

@terdon彼らは間違っている可能性があります-私はそれが死んだことを指摘していました。&とno&の違いを示しました-追加すると、シェルが動揺します。しかし、私は...私は、1つまたは2つのを台無しにしているかもしれませんが、私は、彼らはすべての権利だと思うので、貼り付け/コピーをたくさんやった
mikeserv

sh:2行目:予期しないトークン `| 'の近くの構文エラー
テルドン

@terdonええ、「最初に実行したプロセスを完全に並行してバックグラウンドで実行できません。参照してください。」最初のものはバックグラウンドではありませんが、&を追加しようとすると「予期しないトークン」が追加されます。私が &を使用できるheredocを入手します。
mikeserv 14年
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.