「はい」はどのようにしてファイルにこれほど迅速に書き込みますか?


58

例を挙げましょう:

$ 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

ここでは、コマンドyes115046401 1953秒で行を書き込むのに対してfor、bash とを使用して5秒で行のみを書き込むことができますecho

コメントで示唆されているように、それをより効率的にするためのさまざまなトリックがありますが、どれも速度に匹敵するものはありません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万行になります。良いが、それでもyes1秒間に約1,100万行を書き込むことができるのはまだまだ遠いです!

それでは、ファイルへの書き込みどのyesように高速ですか?



9
2番目の例では、ループの反復ごとに2つの外部コマンド呼び出しがあり、dateやや重いですが、シェルはechoループの反復ごとに出力ストリームを再度開く必要があります。最初の例では、単一の出力リダイレクトを使用した単一のコマンド呼び出しのみがあり、コマンドは非常に軽量です。2つは決して比較できません。
CVn

@MichaelKjörlingはあなたが正しいdateことは重いかもしれません、私の質問の編集を参照してください。
パンディア

1
timeout 1 $(while true; do echo "GNU">>file2; done;)使用するために間違った方法であるtimeout ため、timeoutコマンド置換が終了すると、コマンドのみを起動します。を使用しtimeout 1 sh -c 'while true; do echo "GNU">>file2; done'ます。
ムル

1
回答の要約:write(2)最初の例(dateファイルに出力されるすべての行を実行して待機する)で、他のsyscallのボート負荷、シェルオーバーヘッド、またはプロセスの作成ではなく、システムコールのみにCPU時間を費やします。大量のRAMを搭載した最新のシステムでは、1秒の書き込みでディスクI / O(CPU /メモリではなく)のボトルネックになります。長く実行できる場合、差は小さくなります。(使用するbashの実装の悪さ、およびCPUとディスクの相対速度によっては、bashでディスクI / Oを飽和させることさえできない場合があります)。
ピーターコーデス

回答:


65

一言で言えば:

yes典型的には、他のほとんどの標準ユーティリティと同様の挙動を示す書き込みファイルストリームを介しはlibCによってバッファリングされた出力と標準入出力。これらは、4 write()kb (16 kbまたは64 kb)ごと、または出力ブロックBUFSIZが何であれ、syscallを実行します。echoあるwrite()ごとにGNU。これは多くモード切り替えです (明らかに、コンテキスト切り替えほどコストがかかりません)

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


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

それがyesstdio を使用する前に言ったとき、私はそれがそうするものと多くのように振る舞うので、それがそうであるとだけ仮定しました。これは正しくありませんでした-この方法で動作をエミュレートするだけです。何それは実際に行うことは非常に私はシェルで以下やった事にアナログのようなものです:それは、最初の引数を融合するループ(またはyなしている場合)、彼らは超えずにこれ以上成長しない可能性があるまでBUFSIZ

関連するループ状態の直前のソースからのコメントfor

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

yeswrite()その後、独自の処理を行います。


余談:

(もともと質問に含まれ、すでにここに書かれている可能性のある有益な説明の文脈のために保持されているように)

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

timeoutあなたはコマンド置換を持っている問題-私は今、それを得ると思うし、それは停止しない理由を説明することができます。timeoutコマンドラインが実行されないため、起動しません。シェルは子シェルをフォークし、stdoutでパイプを開いて読み取ります。子が終了すると読み取りを停止し、その後、$IFSマングリングおよびグロブ展開のために書き込まれたすべての子を解釈し、その結果でから$(一致までをすべて置き換えます)

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


その他timeoutの:

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

他の場所で述べたように、[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

これは、前述のコマンドサブ関係のようなものですが、パイプはなく、子は親を殺すまでバックグラウンドになります。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


関連する:

要求されているように、このリンクでここで行われていることに関する単なるコードのコメントよりも詳細な説明があります。


@heemayl-多分?私はあなたが何を求めているのかを完全に理解していないのですか?プログラムがないか、バッファリングしていた出力の書き込みにはstdioを使用する場合(デフォルトでは標準エラー出力など)または行バッファリング(デフォルトで端末に)またはブロック・バッファリングを(基本的には他のほとんどのものは、デフォルトでこのように設定されています)。何が出力バッファのサイズを設定するのか少しわかりませんが、通常は4kbの一部です。したがって、stdio lib関数は、ブロック全体を書き込むことができるまで出力を収集します。ddたとえば、stdioを絶対に使用しない標準ツールの1つです。他のほとんどが行います。
mikeserv

3
シェルバージョンは、open(既存の)writeAND close(まだフラッシュを待機していると思われます)を実行dateしています。また、各プロセスで新しいプロセスを作成し、実行しています。
dave_thompson_085

@ dave_thompson_085- / dev / chatに移動します。そしてあなたが言うように、あなたが言うことは必ずしも真実ではありません。たとえば、私のためにそのwc -lループを実行すると、ループが行うbash出力の5分の1を取得しshますbash-100k writes()からdash500k を少し超えて管理します。
mikeserv

すみません、あいまいでした。私は質問のシェルバージョンを意味しましたが、それを読んだfor((sec0=`date +%S`;...時点では、ループ内の時間とリダイレクトを制御するための元のバージョンのみであり、その後の改善はありませんでした。
-dave_thompson_085

@ dave_thompson_085-その罰金。とにかく、いくつかの基本的なポイントについての答えは間違っていました。
mikeserv

20

より良い質問は、なぜあなたのシェルがファイルを非常にゆっくりと書いているかということです。syscallsを責任を持って(一度にすべての文字をフラッシュするのではなく)ファイルを作成するファイルを使用する自己完結型のコンパイル済みプログラムは、合理的に迅速に実行できます。あなたがしていることは、インタプリタ言語(シェル)で行を書くことであり、さらに多くの不必要な入出力操作をします。何yesが:

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

スクリプトの機能:

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

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


11

他の回答は、主要なポイントに対処しています。補足として、計算の最後に出力ファイルに書き込むことにより、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

はい、これは重要で、筆記速度(少なくとも)は私の場合2倍になります
パンディア
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.