echoとcatの実行時間にこのような違いがあるのはなぜですか?


15

回答この質問は、私は別の質問をさせて:
私は、次のスクリプトは、同じことを行う2つ目は、最初のものを使用しているため、はるかに高速であるべきと思ったcat何度もファイルを開く必要がなく、二つだけのファイルを開きます一度だけ変数をエコーし​​ます:

(正しいコードについては、更新セクションを参照してください。)

最初:

#!/bin/sh
for j in seq 10; do
  cat input
done >> output

第二:

#!/bin/sh
i=`cat input`
for j in seq 10; do
  echo $i
done >> output

入力は約50メガバイトです。

しかし、2番目の方法を試したときは、変数のエコーiが大規模なプロセスだったため、遅すぎました。また、2番目のスクリプトで問題が発生しました。たとえば、出力ファイルのサイズが予想よりも小さかったです。

私はまたのmanページを確認echoし、catそれらを比較します:

echo-テキストの行を表示します

cat-ファイルを連結し、標準出力に出力します

しかし、違いはありませんでした。

そう:

  • なぜ2番目のスクリプトでcatがとても速く、エコーがとても遅いのですか?
  • それとも変数の問題ですiか?(そのマニュアルページで echo「テキスト行」と表示されると言われているので、i。のような非常に長い変数ではなく、短い変数に対してのみ最適化されていると思います。しかし、それは単なる推測です。)
  • そして、使用するときに問題が発生したのはなぜechoですか?

更新

間違って使用するseq 10代わりに使用しました`seq 10`。これは編集されたコードです:

最初:

#!/bin/sh
for j in `seq 10`; do
  cat input
done >> output

第二:

#!/bin/sh
i=`cat input`
for j in `seq 10`; do
  echo $i
done >> output

ロアイマに感謝します。)

ただし、問題のポイントではありません。ループは一度だけ発生した場合でも、私は同じ問題を得る:catはるかに速くよりも動作しますecho


1
そしてどうcat $(for i in $(seq 1 10); do echo "input"; done) >> output?:)
netmonk

2
echo高速です。不足しているのは、変数を使用するときに引用符を付けないことにより、シェルにあまりにも多くの作業をさせているということです。
ロアイマ

変数を引用することは問題ではありません。問題は変数i自体です(つまり、入力と出力の間の中間ステップとして使用します)。
アレクサンダー

`echo $ i`-これをしないでください。printfを使用して引数を引用します。
PSkocik

1
@PSkocik私が言っていることはあなたがしたいですprintf '%s' "$i"、ではありませんecho $i。@cuonglmは、エコーの問題のいくつかを彼の答えでうまく説明しています。エコーを使用する場合に引用でさえ十分ではない理由については、unix.stackexchange.com
questions /

回答:


24

ここで考慮すべきことがいくつかあります。

i=`cat input`

高価になる可能性があり、シェル間に多くのバリエーションがあります。

これは、コマンド置換と呼ばれる機能です。アイデアは、コマンドの出力全体から末尾の改行文字を除いたものiをメモリの変数に保存することです。

そのためには、シェルはコマンドをサブシェルでフォークし、パイプまたはソケットペアを介してその出力を読み取ります。ここには多くのバリエーションがあります。ここの50MiBファイルでは、たとえばbashがksh93の6倍遅いが、zshよりわずかに速く、2倍速いことがわかりますyash

bash遅い主な理由は、パイプから一度に128バイトを読み取り(他のシェルは一度に4KiBまたは8KiBを読み取る)、システムコールのオーバーヘッドによってペナルティを受けるためです。

zshNULバイトをエスケープするためにいくつかの後処理を行う必要があり(他のシェルはNULバイトで中断します)、yashマルチバイト文字を解析することでさらに強力な処理を行います。

すべてのシェルは、多少効率的に実行している可能性のある後続の改行文字を削除する必要があります。

NULバイトを他のバイトよりも優雅に処理し、その存在を確認したい場合があります。

その後、その大きな変数をメモリに格納すると、通常、その操作には、より多くのメモリの割り当てとデータのコピーが含まれます。

ここでは、変数のコンテンツをに渡します(渡すつもりでした)echo

幸いなことに、echoシェルに組み込まれています。そうでなければ、引数リストが長すぎるエラーで実行が失敗する可能性があります。それでも、引数リスト配列の構築には、変数の内容のコピーが含まれることがあります。

コマンド置換アプローチのもう1つの主な問題は、split + glob 演算子を呼び出していることです(変数の引用を忘れることにより)。

そのために、シェルは文字列として文字列を処理するために必要な文字(一部のシェルがないとその点でバグがあるけれども)(のようにまだ行っていない場合の手段は、UTF-8シーケンスを解析することを、UTF-8ロケールではそうyashありません) 、$IFS文字列内の文字を探します。場合は$IFSスペース、(デフォルトの場合)、タブや改行が含まれ、このアルゴリズムは、より複雑で高価です。次に、その分割から生じる単語を割り当ててコピーする必要があります。

glob部分はさらに高価になります。それらの単語のいずれかがグロブ文字が含まれている場合(*?[)、その後、シェルはいくつかのディレクトリの内容を読み、いくつかの高価なパターンマッチングを行う必要があります(bashたとえばの実装は、その時に非常に悪い悪名高いです)。

入力にのようなものが含まれている場合、/*/*/*/../../../*/*/*/../../../*/*/*数千のディレクトリがリストされ、数百MiBに拡張される可能性があるため、非常に高価になります。

その後echo、通常は追加の処理を行います。一部の実装\xでは、受け取った引数内のシーケンスを展開します。これは、コンテンツの解析と、おそらく別のデータの割り当てとコピーを意味します。

一方、OK、ほとんどのシェルにcatはビルトインではないため、プロセスをフォークして実行する(つまりコードとライブラリをロードする)ことを意味しますが、最初の呼び出しの後、そのコードと入力ファイルのコンテンツメモリにキャッシュされます。一方、仲介者はいません。cat一度に大量を読み取り、処理せずにすぐに書き込みます。大量のメモリを割り当てる必要はなく、再利用する1つのバッファだけを割り当てます。

また、NULバイトでチョークせず、後続の改行文字をトリミングしないため、はるかに信頼性が高いことを意味します(変数を引用することで回避できますが、分割+グロブは行いませんが、エスケープシーケンスを展開しますがprintfecho)の代わりに使用することで回避できます。

あなたはさらにそれを最適化したい場合は、代わりに起動するのcatに数回、ただ合格inputに数回cat

yes input | head -n 100 | xargs cat

100ではなく3つのコマンドを実行します。

変数バージョンの信頼性を高めるには、使用する必要がありますzsh(他のシェルはNULバイトに対応できません)。

zmodload zsh/mapfile
var=$mapfile[input]
repeat 10 print -rn -- "$var"

入力にNULバイトが含まれていないことがわかっている場合は、次を使用してPOSIX方式で確実に実行できます(ただし、ビルトインされていない場所でprintfは機能しない場合があります)。

i=$(cat input && echo .) || exit # add an extra .\n to avoid trimming newlines
i=${i%.} # remove that trailing dot (the \n was removed by cmdsubst)
n=10
while [ "$n" -gt 10 ]; do
  printf %s "$i"
  n=$((n - 1))
done

しかし、それはcatループで使用するよりも効率的ではありません(入力が非常に小さい場合を除く)。


引数が長い場合は、メモリ不足になる可能性があることに注意しください。例/bin/echo $(perl -e 'print "A"x999999')
cuonglm

あなたは、読み込みサイズが重要な影響を持っているという仮定に間違われているので、本当の理由を理解するために私の答えを読んでください。
気味悪い

@ schily、128バイトの409600読み取りを行うには、64kの800読み取りよりも多くの時間(システム時間)がかかります。と比較dd bs=128 < input > /dev/nullしてくださいdd bs=64 < input > /dev/null。そのファイルを読み取るためにbashに必要な0.6秒のうち、0.4はread私のテストでそれらのシステムコールに費やされますが、他のシェルはそこでより少ない時間を費やします。
ステファンシャゼル

さて、あなたは実際のパフォーマンス分析を実行していないようです。読み取り呼び出しの影響(異なる読み取りサイズを比較する場合)は約です。関数readwc() およびtrim()Burneシェルでは、全体の1%が全体の30%を占めます。これは、のgprof注釈付きのlibcがないため、おそらく過小評価されmbtowc()ます。
気味悪い

どちらに\x展開されますか?
モハンマド

11

問題はに関するものではありませんcatし、echoそれは忘れ引用変数についてです、$i

Bourneのようなシェルスクリプト(を除くzsh)では、変数を引用符で囲まないglob+splitと、変数の演算子が発生します。

$var

実際には:

glob(split($var))

そのため、ループを繰り返すたびにinput(末尾の改行を除く)の内容全体が展開、分割、グロビングされます。プロセス全体では、シェルがメモリを割り当て、文字列を何度も解析する必要があります。それがあなたが悪いパフォーマンスを得た理由です。

回避するために変数を引用することはできますがglob+split、シェルが大きな文字列引数を構築し、その内容をスキャンする必要があるためecho(組み込みechoを外部に置き換えると/bin/echo引数リストが長すぎるかメモリ不足になるため、あまり役に立ちません$iサイズに依存します)。echo実装のほとんどはPOSIXに準拠していません\x。受け取った引数のバックスラッシュシーケンスを展開します。

を使用するcatと、シェルはループを繰り返すたびにプロセスを生成するだけで、catI / Oのコピーを実行します。システムはファイルの内容をキャッシュして、catプロセスを高速化することもできます。


2
@roaima:glob部分については言及していませんでしたが、これは大きな理由に/*/*/*/*../../../../*/*/*/*/../../../../なる可能性があり、ファイルコンテンツに含まれる可能性のあるものをイメージします。詳細を指摘したいだけです。
クオンルム

ごめんありがとうございます。それがなくても、引用符で囲まれていない変数を使用するとタイミングが倍になります
-roaima

1
time echo $( <xdditg106) >/dev/null real 0m0.125s user 0m0.085s sys 0m0.025s time echo "$( <xdditg106)" >/dev/null real 0m0.047s user 0m0.016s sys 0m0.022s
netmonk

クォートで問題を解決できない理由がわかりませんでした。もっと説明が必要です。
モハマド

1
@ mohammad.k:私の答えで書いたように、変数を引用することはglob+split部分を防ぎ、whileループを高速化します。そして、私はそれがあなたをあまり助けにしないことにも注目しました。以来、ほとんどのシェルのecho動作はPOSIXに準拠していません。printf '%s' "$i"優れている。
クオンルム

2

あなたが電話した場合

i=`cat input`

これにより、シェルプロセスを50MBから最大200MBまで拡張できます(内部ワイドキャラクターの実装に依存)。これによりシェルが遅くなる場合がありますが、これは主な問題ではありません。

主な問題は、上記のコマンドがファイル全体をシェルメモリに読み込むecho $i必要があり、そのファイルの内容をでフィールド分割する必要があることです$i。フィールド分割を行うには、ファイルからのすべてのテキストをワイド文字に変換する必要があり、これがほとんどの時間を費やしています。

私は遅いケースでいくつかのテストを行い、これらの結果を得ました:

  • 最速はksh93です
  • 次は私のボーンシェル(ksh93より2倍遅い)
  • 次にbash(ksh93より3倍遅い)
  • 最後はksh88(ksh93より7倍遅い)

ksh93が最も速い理由は、ksh93がmbtowc()libcからではなく、独自の実装を使用しているためと思われます。

ところで:ステファンは読み取りサイズに何らかの影響があると誤解されています。128バイトではなく4096バイトのチャンクで読み取るようにBourne Shellをコンパイルし、両方のケースで同じパフォーマンスを得ました。


このi=`cat input`コマンドはフィールド分割を行いません、echo $iそれはそうです。上に費やした時間は、i=`cat input`に比べて無視することができるであろうecho $iが、ないに比べてcat input一人で、との場合にはbash、違いが原因に最良の部分のためにあるbash小さな読み取りやって。128から4096に変更してものパフォーマンスには影響しecho $iませんが、それは私が言っていたポイントではありませんでした。
ステファンシャゼル

またecho $i、入力の内容とファイルシステム(IFSまたはグロブ文字が含まれている場合)によってパフォーマンスが大きく異なるため、回答でシェルの比較を行わなかった理由にも注意してください。たとえば、ここの出力ではyes | ghead -c50M、ksh93がすべての中で最も遅いですが、でyes | ghead -c50M | paste -sd: -、それは最も速いです。
ステファンシャゼラス

合計時間について話すときは、実装全体について話していました。もちろん、echoコマンドでフィールド分割が行われます。これは、ほとんどの時間を費やしている場所です。
7

もちろん、パフォーマンスは$ iの内容に依存することは正しいです。
気味悪い

1

どちらの場合も、ループは2回だけ実行されます(単語seqに対して1回、単語に対して1回10)。

さらに、両方とも隣接する空白をマージし、先頭/末尾の空白を削除するため、出力は必ずしも入力の2つのコピーではありません。

最初

#!/bin/sh
for j in $(seq 10); do
    cat input
done >> output

第二

#!/bin/sh
i="$(cat input)"
for j in $(seq 10); do
    echo "$i"
done >> output

echoが遅い理由の1つは、引用符で囲まれていない変数が空白で個別の単語に分割されていることです。50MBの場合、多くの作業が必要になります。変数を引用してください!

これらのエラーを修正し、タイミングを再評価することをお勧めします。


これをローカルでテストしました。の出力を使用して50MBのファイルを作成しましたtar cf - | dd bs=1M count=50。また、ループをx100の係数で実行するように拡張し、タイミングが適切な値にスケーリングされるようにしました(コード全体にループを追加しました:for k in $(seq 100); do... done)。タイミングは次のとおりです。

time ./1.sh

real    0m5.948s
user    0m0.012s
sys     0m0.064s

time ./2.sh

real    0m5.639s
user    0m4.060s
sys     0m0.224s

ご覧のとおり、実際の違いはありませんが、含まれているバージョンのechoほうが少し速く実行されます。引用符を削除して壊れたバージョン2を実行すると、時間が倍になり、シェルは予想されるはずのはるかに多くの作業を行わなければならないことがわかります。

time ./2original.sh

real    0m12.498s
user    0m8.645s
sys     0m2.732s

実際には、ループは2回ではなく10回実行されます。
fpmurphy

あなたが言ったようにやったが、問題は解決されていない。catは、よりも非常に高速ですecho。最初のスクリプトは平均3秒で実行されますが、2番目のスクリプトは平均54秒で実行されます。
モハンマド

@ fpmurphy1:いいえ コードを試しました。ループは2回だけ実行され、10回は実行されません。
モハンマド

3回目の@ mohammad.k:変数を引用すると、問題はなくなります。
ロアイマ

@roaima:コマンドtar cf - | dd bs=1M count=50は何をしますか?内部に同じ文字を含む通常のファイルを作成しますか?もしそうなら、私の場合、入力ファイルはすべての種類の文字と空白を含む完全に不規則です。そして再び、私timeはあなたが使用したように使用し、結果は私が言ったものでした:54秒対3秒。
モハンマド

-1

read よりもはるかに高速です cat

誰でもこれをテストできると思います:

$ cd /sys/devices/system/cpu/cpu0/cpufreq
───────────────────────────────────────────────────────────────────────────────────────────
$ time for ((i=0; i<10000; i++ )); do read p < scaling_cur_freq ; done

real    0m0.232s
user    0m0.139s
sys     0m0.088s
───────────────────────────────────────────────────────────────────────────────────────────
$ time for ((i=0; i<10000; i++ )); do cat scaling_cur_freq > /dev/null ; done

real    0m9.372s
user    0m7.518s
sys     0m2.435s
───────────────────────────────────────────────────────────────────────────────────────────
$ type -a read
read is a shell builtin
───────────────────────────────────────────────────────────────────────────────────────────
$ type -a cat
cat is /bin/cat

cat9.372秒かかります。数秒echoかかります.232

read40倍高速です。

私の最初のテスト$pは、画面にエコーされたとき、read48倍の速さcatでした。


-2

これechoは、画面に1行を配置することを意図しています。2番目の例で行うことは、ファイルの内容を変数に入れてから、その変数を印刷することです。最初のものでは、すぐにコンテンツを画面に配置します。

catこの用途向けに最適化されています。echoではありません。また、環境変数に50Mbを入れることはお勧めできません。


奇妙な。echoテキストを書き出すために最適化されないのはなぜですか?
ロアイマ

2
POSIX標準には、エコーが画面に1行を表示することを意図していると言うものは何もありません。
fpmurphy

-2

エコーが速くなるということではなく、あなたがしていることに関するものです。

ある場合には、入力から読み取り、出力に直接書き込みます。つまり、catを介して入力から読み取られたものはすべて、stdoutを介して出力に送られます。

input -> output

もう1つの場合は、入力からメモリ内の変数に読み取り、変数の内容を出力に書き込みます。

input -> variable
variable -> output

後者は、特に入力が50MBの場合、はるかに遅くなります。


猫は、stdinからコピーしてstdoutに書き込むことに加えて、ファイルを開く必要があることを言及する必要があると思います。これは2番目のスクリプトの卓越性ですが、最初のスクリプトは合計で2番目のスクリプトよりも非常に優れています。
モハンマド

2番目のスクリプトには卓越性はありません。どちらの場合でも、catは入力ファイルを開く必要があります。最初のケースでは、catの標準出力はファイルに直接移動します。2番目の場合、catのstdoutは最初に変数に移動し、次に変数を出力ファイルに出力します。
アレクサンダー

@ mohammad.k、2番目のスクリプトには「卓越性」は強調されていません。
ワイルドカード
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.