Bashで探す

Bashで探す

ファイルを繰り返すには2つの方法があります。

  1. -ループの使用for

    for f in *; do
        echo "$f"
    done
    
  2. 使用find:

    find * -prune | while read f; do 
        echo "$f"
    done
    

2つのループが同じファイルのリストを見つけると仮定すると、2つのオプションの違いは何ですかパフォーマンスそして処理?

答え1

私は2259個のエントリがあるディレクトリでこれを試み、timeコマンドを使用しました。

出力time for f in *; do echo "$f"; done(ファイルを除く!)は次のとおりです。

real    0m0.062s
user    0m0.036s
sys     0m0.012s

出力time find * -prune | while read f; do echo "$f"; done(ファイルを除く!)は次のとおりです。

real    0m0.131s
user    0m0.056s
sys     0m0.060s

キャッシュミスを排除するために、各コマンドを複数回実行します。これは、出力を使用してパイピングする(について)を維持することbash(for i in ...)がより速いことを示しています。findbash

完璧にするために、findあなたの例ではパイプが完全に重複しているので、パイプを削除しました。ただ出力は次find * -pruneのとおりです

real    0m0.053s
user    0m0.016s
sys     0m0.024s

またtime echo *(出力は改行で区切られません。)

real    0m0.009s
user    0m0.008s
sys     0m0.000s

echo *この時点でより速い理由は、改行をあまり出力しないため、出力があまりスクロールしないためだと思います。テストしてみましょう...

time find * -prune | while read f; do echo "$f"; done > /dev/null

生産する:

real    0m0.109s
user    0m0.076s
sys     0m0.032s

そしてtime find * -prune > /dev/null出力は次のようになります。

real    0m0.027s
user    0m0.008s
sys     0m0.012s

そしてtime for f in *; do echo "$f"; done > /dev/null以下を作ります:

real    0m0.040s
user    0m0.036s
sys     0m0.004s

最後に:time echo * > /dev/null収率:

real    0m0.011s
user    0m0.012s
sys     0m0.000s

いくつかの変動はランダムな要因として説明されるかもしれませんが、これは明らかです。

  • 出力速度が遅い
  • パイプラインのコストが少し
  • for f in *; do ...find * -pruneそれ自体より遅いですが、パイプに関連する胃構造の場合は高速です。

また、どちらの方法も空白のある名前をうまく処理するようです。

編集する:

find . -maxdepth 1 > /dev/null時間が経つにつれてfind * -prune > /dev/null

time find . -maxdepth 1 > /dev/null:

real    0m0.018s
user    0m0.008s
sys     0m0.008s

find * -prune > /dev/null:

real    0m0.031s
user    0m0.020s
sys     0m0.008s

したがって、追加の結論は次のとおりです。

  • find * -prune以前よりも遅い場合は、シェルが find . -maxdepth 1globを処理してからfindfind . -prune.

その他のテスト time find . -maxdepth 1 -exec echo {} \; >/dev/null::

real    0m3.389s
user    0m0.040s
sys     0m0.412s

結論として:

  • これまでに最も遅い方法です。このアプローチを提案する回答の説明で指摘したように、各引数に対してシェルが生成されます。

答え2

1.

最初:

for f in *; do
  echo "$f"
done

-n-eファイル名にバックスラッシュが含まれる一部のbashディストリビューションでは、名前とバリアントのファイルに対して-nene失敗します。

第二:

find * -prune | while read f; do 
  echo "$f"
done

多くの場合、失敗します(!、、、、、名前が-Hスペースで始まるか終わるか、改行文字を含むファイル名...)-name(

引数で受け取ったファイルを印刷する以外に何もしない*拡張シェルです。代わりにfind組み込みを使用または回避することもできますprintf '%s\n'printfパラメータが多すぎます。潜在的なエラー。

2.

拡張は*ソートされ、ソートが不要な場合は速度が速くなる可能性があります。存在するzsh

for f (*(oN)) printf '%s\n' $f

または簡単に:

printf '%s\n' *(oN)

bash私が知っている限り、それに対応するものがないので、に頼らなければなりませんfind

三。

find . ! -name . -prune ! -name '.*' -print0 |
  while IFS= read -rd '' f; do
    printf '%s\n' "$f"
  done

(上記ではGNU / BSDの非-print0標準拡張を使用しています)。

これにはまだ find コマンドの生成と遅いループの使用が含まれるため、ファイルのリストが非常に大きくない限り、ループを使用するよりも遅くなる可能性がありますwhile readfor

4.

また、シェルワイルドカード拡張とは異なり、各ファイルに対してシステムコールを実行するため、非find整列lstatではこれを補償する可能性はありません。

GNU / BSDの場合、最適化保存を実行するfind拡張機能を使用するとこれを回避できます。-maxdepthlstat

find . -maxdepth 1 ! -name '.*' -print0 |
  while IFS= read -rd '' f; do
    printf '%s\n' "$f"
  done

ファイル名の出力が見つかるとすぐに開始されるためfind(stdio出力バッファを除く)、ループで実行するのに時間がかかり、ファイル名のリストがstdioバッファ(4/8kB)より大きい場合。この場合、ループ内処理はfindすべてのファイル検索が完了する前に開始されます。 GNUおよびFreeBSDシステムでは、stdbufこれを使用してより速く実行できます(stdioバッファリングを無効にする)。

5.

各ファイルに対してコマンドを実行するPOSIX /標準/移植可能な方法は、述語をfind使用することです。-exec

find . ! -name . -prune ! -name '.*' -exec some-cmd {} ';'

ただし、この場合、シェルで繰り返すよりも効率が低くなります。シェルには、新しいプロセスを作成し、各ファイルに対して実行する必要があるwhileechoの組み込みバージョンがあるためです。echofind/bin/echo

複数のコマンドを実行する必要がある場合は、次のことができます。

find . ! -name . -prune ! -name '.*' -exec cmd1 {} ';' -exec cmd2 {} ';'

cmd2ただし、成功した場合にのみ実行されることに注意してください。cmd1

6.

各ファイルに対して複雑なコマンドを実行する標準的な方法は、次を使用してシェルを呼び出すことです-exec ... {} +

find . ! -name . -prune ! -name '.*' -exec sh -c '
  for f do
    cmd1 "$f"
    cmd2 "$f"
  done' sh {} +

この時点では、組み込みバージョンをecho使用し、できるだけ少ない数のバージョンを作成するため、効率が再び向上します。sh-exec +sh

7.

存在する200,000個のファイルがあるディレクトリでテスト中です。ext4の短い名前の場合、最初のzsh項目(セクション2)は最も速く、最初の単純なfor i in *ループはそれに従います(通常はbash他のシェルよりはるかに遅いです)。

答え3

私は間違いなく検索を選択しますが、検索を次のように変更します。

find . -maxdepth 1 -exec echo {} \;

もちろん、パフォーマンスの面ではfind必要に応じてはるかに高速です。現在持っているのは、forディレクトリの内容ではなく、現在のディレクトリのファイル/ディレクトリだけを表示します。 find を使用すると、サブディレクトリの内容も表示されます。

forまず拡張しなければならないので find がより良いと言って*、ファイルが多いディレクトリがあるとエラーが発生するか心配されます。パラメータリストが長すぎます。。以下にも適用されます。find *

たとえば、現在作業中のシステムには、200万を超えるファイル(それぞれ10万未満)を含む複数のディレクトリがあります。

find *
-bash: /usr/bin/find: Argument list too long

答え4

しかし、私たちはパフォーマンスの問題にこだわっています!実験に対するこの要求は、妥当性を低下させる少なくとも2つの仮定をします。

A. 同じファイルが見つかったとします。

まあ、彼ら〜する同じファイルがすべて同じglobを繰り返すので、最初に見つかります。つまり、*いくつfind * -prune | while read fかの欠陥があり、予想されるすべてのファイルが見つからない可能性があります。

  1. POSIX findは複数のパスパラメータを受け入れることは保証されていません。ほとんどのfind実装ではこれを行いますが、それに依存してはいけません。
  2. find *ぶつかると壊れますARG_MAXfor f in *いいえ。組み込み機能ではなくARG_MAXに適用されるからです。exec
  3. while read fスペースで始まり、終わるファイル名を区切ることができ、スペースは削除されます。while read既定のパラメーターを使用してこの問題を克服できますが、REPLYファイル名に改行が含まれている場合はまだ役に立ちません。

B..echo誰もファイル名をエコーするためにこれを行いません。これを行うには、次のいずれかを実行します。

printf '%s\n' *
find . -mindepth 1 -maxdepth 1 # for dotted names, too

ここでループされたパイプは、ループのwhile端で閉じる暗黙のサブシェルを生成しますが、これは一部の人にとって直感的ではないかもしれません。

この質問に答えるために、184個のファイルとディレクトリを含む私のディレクトリの結果は次のとおりです。

$ time bash -c 'for i in {0..1000}; do find * -prune | while read f; do echo "$f"; done >/dev/null; done'

real    0m7.998s
user    0m5.204s
sys 0m2.996s
$ time bash -c 'for i in {0..1000}; do for f in *; do echo "$f"; done >/dev/null; done'

real    0m2.734s
user    0m2.553s
sys 0m0.181s
$ time bash -c 'for i in {0..1000}; do printf '%s\n' * > /dev/null; done'

real    0m1.468s
user    0m1.401s
sys 0m0.067s

$ time bash -c 'for i in {0..1000}; do find . -mindepth 1 -maxdepth 1 >/dev/null; done '

real    0m1.946s
user    0m0.847s
sys 0m0.933s

関連情報