「はい」を使用すると、ファイルにどのようにすばやく書き込むことができますか?

「はい」を使用すると、ファイルにどのようにすばやく書き込むことができますか?

たとえば、見てみましょう。

$ 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

yesここでは、コマンドは1秒以内に行を書きますが、私はbashを使用して115046405秒で行を書くことができることがわかります。1953forecho

コメントで提案されているように、効率を向上させるさまざまな方法がありますが、その速度に近い方法はありません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

毎秒最大20,000行を書き込むことができます。次のようにさらに改善することができます。

$ 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

これにより、毎秒40,000行を得ることができます。より良いですが、yesそれでも毎秒1,100万行を書くのは遠いです!

だから、yesなぜファイルはそんなに速く書かれますか?

答え1

簡単に言うと:

yes通常、他のほとんどの標準ユーティリティと同様の動作を示します。書くファイルストリーム出力は以下を介してlibCによってバッファリングされます。stdio。これらはシステムコールのみを実行します。write()それぞれ約4kb(16kbまたは64kb)または出力ブロックBUFSIZはい。echowrite()GNU。それはたくさん~のモード切替 (もちろんそうではありませんが、コンテキストスイッチ)

言うまでもなく、初期最適化ループ以外はyes非常にシンプルで小さなコンパイルされたCループなので、シェルループはコンパイラオプティマイザと比較できません。


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

私が以前yesuseと言ったときstdio、私はそれがそうする人と非常によく似ているので、そうだったと思いました。それは本当ではありません。ただ彼らの行動をそのように真似するだけです。実際に行うことは、シェルを使用して以下で行うことと非常によく似ています。まず、引数のマージを繰り返します。(またはyそうでない場合)もう成長しなくなるまでそうではありませんBUFSIZ

さんのコメント源泉関連ループ状態の直前for:

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

yesその後、自分のwrite()作業を行います。


余談:

(元の質問に含まれており、ここで作成された潜在的に有益な説明の文脈のままです。):

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

コマンドの代替の問題timeout- 理解し、なぜ停止しないのかを説明できるようです。timeoutコマンドラインが実行されていないため起動しません。シェルはサブシェルを分岐し、標準出力でパイプを開き、それを読み取ります。サブプロセスが終了したら、読み取りを停止し、再構成および$IFSグローバル拡張のために作成されたすべてのサブプロセスを解釈し、結果に従って一致$(するすべての項目を置き換えます)

ただし、子プロセスがパイプに書き込まれない無限ループの場合、子プロセスはループを停止せず、timeoutコマンドラインも決して停止しません。(私が推測した通り)Ctrl+を実行しCてサブループを終了します。だからtimeout確認 いいえ始める前に完了する必要があるループを終了します。


その他timeout:

...シェルプログラムが出力を処理するためにユーザーモードとカーネルモードを切り替えるのに必要な時間ほど、パフォーマンスの問題とは関係ありません。timeoutしかし、シェルほど柔軟ではありません。シェルの利点は、パラメータを処理し、他のプロセスを管理する能力にあります。

他の場所で指摘したように[fd-num] >> named_file単にループコマンドの出力を指示するのではなく、ループの出力先にリダイレクトすると、パフォーマンスが大幅に向上する可能性があります。open()システムコールは一度だけ完了できます。|これは、ターゲットが内部ループの出力であるパイプを使用して以下でも実行されます。


直接比較:

あなたは次のようになります:

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


費用は次のとおりです。

リクエストに応じて、単純なコードコメントよりも包括的な説明をここで見ることができます。このリンク

答え2

より良い質問は、シェルがファイルの書き込み速度が遅すぎる理由です。ファイル書き込みシステムコールを責任を持って使用するスタンドアロンコンパイラ(すべての文字を一度にフラッシュする代わりに)は、これをかなり迅速に実行します。あなたがやっていることは説明した言語(シェル)、さらにあなたはたくさん不要な入出力操作。何yesですか:

  • 書き込み用にファイルを開く
  • 最適化されコンパイルされた関数を呼び出してストリームに書き込む
  • ストリームはバッファリングされているため、システムコール(カーネルモードへの高価な移行)は大きな塊でほとんど発生しません。
  • ファイルを閉じる

スクリプトの機能は次のとおりです。

  • コード一行を読む
  • コードを解釈し、入力内容を実際に解析し、何をするのかを理解するために、多くの追加作業を行います。
  • whileループの各反復について(通訳言語では安くはないかもしれません):
    • 外部コマンドを呼び出しdateてその出力を保存します(元のバージョンでのみ可能 - 修正されたバージョンではこれを行わないと10倍のゲインが得られます)
    • ループ終了条件が満たされているかテスト
    • 開いている追加モードのファイル
    • コマンドを解析しecho、それをシェル組み込みとして識別し(一部のパターン一致コードを使用して)、引数拡張と引数 "GNU"の他のすべてを呼び出し、最後に開かれたファイルに行を作成します。
    • 閉鎖ファイルをもう一度
    • このプロセスを繰り返します

高価な部分:全体の解釈コストは非常に高価です(bashはすべての入力に対して多くの前処理を実行します。文字列には、変数置換、プロセス置換、中括弧拡張、エスケープ文字などが含まれる場合があります)。そして組み込み関数へのすべての呼び出しが必要です。組み込み関数を処理する関数にリダイレクトするスイッチステートメントであり、各出力行に対してファイルを開閉することが非常に重要です。>> filewhileループの外側に配置すると、これを行うことができます。はるかに早くしかし、まだ通訳された言語を使用しています。これがecho外部コマンドではなく組み込みシェルであることは幸運です。それ以外の場合、ループは各反復ごとに新しいプロセス(フォークと実行)を生成します。これによりプロセスが中断されます。dateループでコマンドを使用すると、これがどのくらい費用がかかるかがわかります。

答え3

他の答えがポイントに達しました。参考までに、計算の終わりに出力ファイルに書き込むことで、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

関連情報