web-dev-qa-db-ja.com

「はい」はどのようにしてファイルにすばやく書き込むのですか?

例を挙げましょう。

$ timeout 1 yes "GNU" > file1
$ wc -l file1
11504640 file1
$ for ((sec0=`date +%S`;sec<=$(($sec0+5));sec=`date +%S`)); do echo "GNU" >> file2; done
$ wc -l file2
1953 file2

ここで、コマンドyes11504640行を1秒で書き込めますが、書き込みは1953行は5秒でbashのforechoを使用します。

コメントで示唆されているように、より効率的にするためのさまざまなトリックがありますが、yesの速度に匹敵するものはありません。

$ ( while :; do echo "GNU" >> file3; done) & pid=$! ; sleep 1 ; kill $pid
[1] 3054
$ wc -l file3
19596 file3
$ timeout 1 bash -c 'while true; do echo "GNU" >> file4; done'
$ wc -l file4
18912 file4

これらは、1秒間に最大2万行を書き込むことができます。さらに、次のようにさらに改善できます。

$ timeout 1 bash -c 'while true; do echo "GNU"; done >> file5' 
$ wc -l file5
34517 file5
$ ( while :; do echo "GNU"; done >> file6 ) & pid=$! ; sleep 1 ; kill $pid
[1] 5690
$ wc -l file6
40961 file6

これにより、1秒間に最大4万行が得られます。 1秒間に約1100万行を書き込むことができるyesとは程遠いですが、

それで、yesはどのようにファイルにすばやく書き込むのですか?

58
Pandya

一言:

yesは、通常 write から FILE STREAM に出力がバッファリングされる他のほとんどの標準ユーティリティと同様の動作を示します。 stdio によるlibCこれらは、syscall write() を4kbごとにのみ実行します(16kbまたは64kb)または出力ブロック [〜#〜] bufsiz [〜#〜] はです。 echoGNUごとのwrite()です。これはlot of mode-switching(これは明らかに コンテキストほどコストがかかりません)スイッチ

最初の最適化ループに加えて、yesは非常にシンプルで小さなコンパイル済みCループであり、シェルループはコンパイラー最適化プログラムに匹敵するものではありません。


しかし、私は間違っていました:

以前にyesがstdioを使用することを言ったとき、私はそれがそうであると思っていました。これは正しくありませんでした。この方法で動作をエミュレートするだけです。それが実際に行うことは、シェルを使用して以下で行ったものと非常に似ています。最初にループして、引数を統合します(またはyなしの場合)成長しなくなるまでBUFSIZを超えずに。

関連するforループの直前にある source からのコメントは次のとおりです。

/* Buffer data locally once, rather than having the
large overhead of stdio buffering each item.  */

yesは、その後独自のwrite() sを実行します。


余談:

(元々質問に含まれており、すでにここに書かれた有益な説明へのコンテキストのために保持されている)

timeout 1 $(while true; do echo "GNU">>file2; done;)を試しましたが、ループを停止できません。

コマンド置換で発生するtimeoutの問題-私は今それを理解していると思いますが、停止しない理由を説明できます。 timeoutは、コマンドラインが実行されないため、起動しません。シェルは子シェルをフォークし、その標準出力でパイプを開いて読み取ります。子が終了すると読み取りを停止し、次に$IFSのマングリングとグロブの展開用に書き込まれたすべての子を解釈し、その結果、$(から対応する)までのすべてを置き換えます。

しかし、子がパイプに書き込みを行わない無限ループである場合、子はループを停止することはなく、timeoutのコマンドラインは(推測どおり)の前に完了しません。 CTRL-Cおよび子ループを強制終了します。したがって、timeout can never開始する前に完了する必要があるループを強制終了します。


その他timeouts:

... Shellプログラムが出力を処理するためにユーザーモードとカーネルモード間の切り替えに費やす必要がある時間の長さほど、パフォーマンスの問題とはそれほど関係ありません。ただし、timeoutは、シェルがこの目的に使用できるほど柔軟ではありません。Excelは、引数をマングルし、他のプロセスを管理する機能を備えています。

他の場所で説明されているように、ループの出力先にループするだけでなく、ループの出力先に[fd-num] >> named_fileリダイレクトを移動するだけで、少なくとも のパフォーマンスが大幅に向上します。 open() syscallは一度だけ実行する必要があります。これも、内部ループの出力としてターゲットに設定された|パイプを使用して以下で実行されます。


直接比較:

あなたは好きかもしれません:

for cmd in  exec\ yes 'while echo y; do :; done'
do      set +m
        sh  -c '{ sleep 1; kill "$$"; }&'"$cmd" | wc -l
        set -m
done

256659456
505401

これはkindのコマンドサブ関係と同じですが、パイプはなく、子が親を強制終了するまでバックグラウンドで処理されます。 yesの場合、子が生成されてから親は実際に置き換えられていますが、シェルは独自のプロセスを新しいプロセスでオーバーレイすることによりyesを呼び出します。これにより、PIDは同じままで、そのゾンビが子供はまだ結局誰を殺すか知っています。


大きなバッファ:

シェルのwrite()バッファーの増加について見てみましょう。

IFS="
";    set y ""              ### sets up the macro expansion       
until [ "${512+1}" ]        ### gather at least 512 args
do    set "$@$@";done       ### exponentially expands "$@"
printf %s "$*"| wc -c       ### 1 write of 512 concatenated "y\n"'s  

1024

1kbを超える出力文字列が個別のwrite()に分割されていたため、この番号を選択しました。そしてここに再びループがあります:

for cmd in 'exec  yes' \
           'until [ "${512+:}" ]; do set "$@$@"; done
            while printf %s "$*"; do :; done'
do      set +m
        sh  -c $'IFS="\n"; { sleep 1; kill "$$"; }&'"$cmd" shyes y ""| wc -l
        set -m
done

268627968
15850496

これは、前回のテストよりも、このテストの同じ時間内にシェルによって書き込まれたデータ量の300倍です。汚すぎる格好はやめて。しかし、それはyesではありません。


関連:

this link でここで行われていることについての単なるコードコメントよりも、要求されたとおりの詳細な説明があります。

65
mikeserv

より良い質問は、シェルがファイルを非常にゆっくりと書き込む理由です。責任を持って(一度にすべての文字をフラッシュするのではなく)ファイル書き込みシステムコールを使用する自己完結型のコンパイル済みプログラムは、それをかなり迅速に実行します。あなたがやっていることは、解釈された言語(シェル)で行を書いていて、それに加えて不必要な入出力操作のロットyesが行うこと:

  • 書き込み用にファイルを開きます
  • ストリームに書き込むために最適化およびコンパイルされた関数を呼び出します
  • ストリームはバッファリングされているため、syscall(カーネルモードへの負荷の高いスイッチ)が非常にまれに、大きなチャンクで発生します。
  • ファイルを閉じます

スクリプトの機能:

  • コード行を読み取ります
  • コードを解釈し、入力を実際に解析して何をすべきかを理解するための多くの追加操作を行います
  • whileループの反復ごとに(インタプリタ言語ではおそらく安価ではありません):
    • date外部コマンドを呼び出してその出力を保存します(元のバージョンでのみ-改訂バージョンでは、これを行わないことで10倍になります)
    • ループの終了条件が満たされているかどうかをテストする
    • openファイルを追加モードで開く
    • echoコマンドを解析し、それを(いくつかのパターンマッチングコードで)シェル組み込みとして認識し、引数「GNU」でパラメーター展開およびその他すべてを呼び出し、最後に開いているファイルに行を書き込みます
    • closeファイルをもう一度
    • プロセスを繰り返す

高価な部分:全体の解釈は非常に高価です(bashはすべての入力の非常に多くの前処理を行っています-文字列に変数置換、プロセス置換、中括弧拡張、エスケープ文字などが含まれる可能性があります)、組み込みのすべての呼び出しはおそらく、組み込みを処理する関数へのリダイレクトを伴うswitchステートメントであり、非常に重要なことに、出力のすべての行のファイルを開いたり閉じたりします。あなたは>> file whileループの外ではるかに速くにしますが、まだインタプリタ言語です。 echoが外部コマンドではなくシェルの組み込みであることは非常に幸運です。それ以外の場合、ループはすべての反復で新しいプロセス(fork&exec)を作成することになります。これはプロセスを停止して停止させます-ループにdateコマンドがあると、どれほどのコストがかかるかがわかりました。

20
orion

他の答えは主要なポイントに対処しました。余談ですが、計算の最後に出力ファイルに書き込むことで、whileループのスループットを向上させることができます。比較:

$ i=0;time while  [ $i -le 1000 ]; do ((++i)); echo "GNU" >>/tmp/f; done;

real    0m0.080s
user    0m0.032s
sys     0m0.037s

$ i=0;time while  [ $i -le 1000 ]; do ((++i)); echo "GNU"; done>>/tmp/f;

real    0m0.030s
user    0m0.019s
sys     0m0.011s
11
Apoorv Gupta