
この単純なbashスクリプトがあります。
#!/bin/bash
for i in {1..1000000}
do
echo "hello ${i}"
done
これにより、メッセージが百万回印刷されます。
tee
すべての出力を単一のファイルにダンプすることと、分割出力を2つのファイルとして使用するパフォーマンスを比較したいと思います。
$ time ./run.sh > out1.txt
real 0m9.535s
user 0m6.678s
sys 0m2.803s
$
$ time ./run.sh | tee out2.txt > out1.txt
real 0m6.705s
user 0m6.895s
sys 0m5.214s
2つのファイルに同時に書き込むことは、1つのファイルにのみ書き込むよりも速いことがわかりました(これらの結果は複数の実行で一貫しています)。
これがどのように可能であるかを説明できる人はいますか?また、sumの出力をどのようにuser
解釈する必要がありますか?なぜ使用するのに時間がかかりますか?sys
time
sys
tee
答え1
まず、いくつかのことを明確にしましょう。
- 紅潮
echo
シェルは、この例のように、各組み込みコマンドの後に出力をフラッシュします。
そうする必要があります。これは、外部コマンドであるかのように動作を模倣する必要があります(ここでは/bin/echo
パフォーマンス上の理由から、純粋に外部コマンドではなく組み込みシェルコマンドです)。外部コマンドは、終了時に出力をフラッシュ(または無視)する必要があります。組み込み関数はecho
同じ方法で動作する必要があります。
これは、-edデータがすでに出力に使用可能な場合に予想される動作です(スクリプトが長いecho
待機または計算に続く場合)。echo
この動作は、標準出力が接続されている位置とは無関係です。端末、パイプ、ファイルでも構いません。それぞれはecho
独立してフラッシュされます。 (比較しないでください。基本ご覧のとおり、標準出力が端末に送信されるかどうかによって動作が変わるlibcの動作最大cat
、、grep
などの標準ユーティリティはhead
もちろんtee
。シェルはlibcのデフォルトのバッファリングに依存せず、各組み込みコマンドの後に明示的にフラッシュされます。 )
シェルで実行された百万の呼び出しを表示するstrace ./run.sh > out1.txt
ために使用されます。write()
- マルチコア
システムに複数のCPUコアがあり、他のプロセスからかなりの負荷がないとします。この設定では、コアはbash run.sh
1つのコアに割り当てられ、tee
別のコアに割り当てられます。これにより、重いプロセス遷移は発生せず、実際にすべて実行している場合は同時に実行されます。
おそらく両方のプロセスを単一のコアに制限すると(コマンドを使用してこれを実行できると思いますtaskset
。試してみます)、非常に異なる結果が得られ、tee
プロセスが大幅に遅くなります。シリアルで実行されインターリーブする必要がある追加プロセスだけでなく、run.sh
カーネルも2つのプロセス間で何度も切り替える必要があり、この移行自体のコストはかなり高くなります。
time
コマンド
time
パイプ全体、run.sh
結合tee
パイプを測定します。コマンドの 1 つだけを測定するには、time
サブシェルから呼び出します。たとえば、次のようになります。
$ ( time ./run.sh ) | tee out2.txt > out1.txt
$ ./run.sh | ( time tee out2.txt ) > out1.txt
時間はreal
壁時計に経過時間を印刷します。つまり、文字通りパイプの前後のタイムスタンプを印刷して差を計算したり、外部ストップウォッチを使用したのと同じです。 2つのプロセスがパイプラインで実行され、それぞれが10秒間1つのCPUコアを回転し、両方が完全に並列に永久に実行できる場合、実際の時間は10秒になります。
user
sys
ただし、時間はCPUコア全体に蓄積されます。それぞれ独自のCPUコアにある2つの並列プロセスが、CPUを最大10秒(今見たように実際の時間は10秒)まで回転させると、ユーザー時間は20秒になります。
それではこれを整理してみましょう。
答えるべき質問は一つだけです。ファイルよりもパイプに小さなデータの塊を書く方が速いのはなぜですか?
私はこれに対する直接的な答えを持っていません。私は逆さまに作業し、測定したタイミング結果に基づいて結論を下します。でなければならないパイプへの書き込みはファイルへの書き込みよりも高速です。以下は少し推測ですが、合理的であることを願っています。
パイプのサイズは固定されています(私の考えでは64kB)。パイプを作成するときにフルサイズを割り当てるのは簡単であるため、カーネルで動的割り当てが発生しなくなりました。 (サイズに達すると、リーダーがスペースを解放するまで書き込み側がブロックされます。)しかし、ファイルにはこの制限はありません。ユーザー空間からカーネルに渡されるすべてのコンテンツはそこにコピーする必要があります(データが実際にディスクに書き込まれる前にライターをブロックすることは不可能です)。だから私はこれがおそらくある種の動的メモリ割り当てであることを見つけました。可能ファイルへの書き込み中に発生するため、この部分のコストが高くなります。
パイプを使用すると、カーネルが実行する必要がある唯一の追加の作業は、実行したばかりのプロセスを目覚めさせることです。つまり、データがパイプに表示されるのを待ちます。ファイルの場合、カーネルはファイルのメモリ内メタデータ(サイズ、変更時間)を更新し、タイマーを開始(または既存のタイマーを更新)して、このデータをディスクに最終的に書き込むようにスケジュールする必要があります。
ファイルに書き込むことが重要であるという厳格なルールはありません。しなければならない測定した数字からわかるように、パイプに書くよりもコストがかかります。
を追加すると、100万秒が安くなり、tee
必要な作業が少なくなります。これにより、システム全体がより速く実行され、結果として壁時計時間が短縮されます。run.sh
write()
run.sh
ほとんどは並列に実行され、作業量が少ない2番目のプロセスを追加します。両方の出力ファイルはバッファリングを使用しますwrite()
。つまり、バッファリングされていない場合に比べて、システムコールが数回しかありません。入力には百万の小さなread()
タスクを実行できますが、推測する時間のランダム性やその他の理由により、多くのbash
sがwrite()
組み合わされて1つのsで到着する可能性があるため、read()
100万個未満のread()
sが必要になる場合があります。 (read()
何度も実行されていることを確認すると良いでしょう。strace
"ing"は測定自体がタイミングを大幅に変更するため、オプションではありません。tee
各カウンタに対してカウンタをインクリメントし、最後にread()
その数をダンプするようにパッチします。皆さんの練習問題として残しておきます)。
したがって、パイプラインの完了を遅らせず、tee
速度が速くなります。run.sh
ただし、ユーザーとシステムの時間に独自の共有を追加して、以前よりも大きくします。
修正する:
気になってtee
何度も触れようとしましたread()
。
デスクトップに端末が1つだけあれば66万~67万程度になります。バックグラウンドでブラウザを開き、1〜2ページを表示すると、約500,000〜600,000程度です。ブラウザが始まったばかりで(追加作業)、約400,000個です。これは意味があります。実行する必要がある他の作業が多いほど、tee
そのデータがすぐに読み取られず、一部のbash
「write()
」が蓄積される可能性が高くなります。アイデアを得て、今はおおよその数字を得ました。もちろん、コンピュータによって大きく異なる場合があります。