この Unix コマンドを最適化するにはどうすればよいですか?

この Unix コマンドを最適化するにはどうすればよいですか?

次のコマンドは結果を出力するのに約10分かかります

find . -name "muc*_*_20160920_*.unl*" | xargs zcat |
    awk -F "|" '{if($14=="20160920100643" && $22=="567094398953") print $0}'| head

パフォーマンスを向上させるにはどうすればよいですか?

答え1

これはすでにかなり最適化されています。次のような詳細を知らなければ、ボトルネックが何であるかを知ることは困難です。

  • ストレージの種類 (HD、SSD、ネットワーク、RAID)
  • 一致するファイルの数と平均サイズ
  • ディレクトリとその他の一致しないファイルの数
  • 各行のフィールド数
  • 線の平均長さ

どのような場合でもできること:

  • / がそれをサポートしている場合は、またはに置き換えます。これは間違っている-print | xargsだけでなく、文字をデコードしてどの文字が空白であるかを調べ、コストのかかる引用符処理を実行する必要があるため、コストも高くなります。-exec cmd {} +-print0 | xargs -r0findxargs-print | xargsxargs
  • ロケールを C ( export LC_ALL=C) に固定します。ここで関係するすべての文字 (|およびファイル内容の 10 進数字、ファイル名のラテン文字、ピリオド、およびアンダースコア) はポータブル文字セットの一部であるため、文字セットが UTF-8 またはその他のマルチバイト文字セットである場合は、シングルバイト文字セットの C に切り替えると、および の作業が大幅に軽減されfindますawk
  • awk部分を次のように簡略化しますawk -F "|" '$14 == "20160920100643" && $22 == "567094398953"'
  • 出力を にパイプしているので、 の出力バッファリングを無効にして、できるだけ早く 10 行を出力するようにするとheadいいでしょう。または を使用する場合、そのために を使用できます。または、に を追加することもできます。awkgawkmawkfflush()if (++n == 10) exitawk

総括する:

(export LC_ALL=C
find . -name "muc*_*_20160920_*.unl*" -exec zcat {} + |
  awk -F "|" '$14 == "20160920100643" && $22 == "567094398953" {
    print; if (++n == 10) exit}')

CPU がボトルネックになっている場合は、マルチコア GNU システムで以下を試すことができます。

(export LC_ALL=C
find . -name "muc*_*_20160920_*.unl*" -print0 |
  xargs -r0P 4 -n 100 sh -c '
    zcat "$@" | 
      awk -F "|" "\$14 == "20160920100643" && \$22 == "567094398953" {
        print; fflush()}"' sh | head)

100 個のファイルのバッチで4 つのジョブを並行して実行しますzcat | awk

20160920100643それがタイムスタンプである場合、それ以前に最後に変更されたファイルを除外したい場合があります。GNU または BSD ではfind、 を追加します-newermt '2016-09-20 10:06:42'

行に多数のフィールドがある場合、awk行を分割して多数の$nフィールドを割り当てるとペナルティが発生します。最初の 22 フィールドのみを考慮するアプローチを使用すると、処理速度が向上する可能性があります。

grep -E '^([^|]*\|){13}20160920100643(\|[^|]*){7}\|567094398953(\||$)'

コマンドの代わりに を使用しますawk。GNU ではgrep--line-buffered並列アプローチでできるだけ早く行を出力するオプション、または-m 10非並列アプローチで 10 回の一致後に停止するオプションを追加します。

要約すると、CPU がボトルネックであり、システムに少なくとも 4 つの CPU コアがあり、少なくとも 400 個の muc* ファイルがあり、GNU システム (通常はgrepGNU よりも大幅に高速awk) を使用している場合は、次のようになります。

(export LC_ALL=C
find . -name "muc*_*_20160920_*.unl*" -newermt '2016-09-20 10:06:42' -print0 |
  xargs -r0P 4 -n 100 sh -c '
    zcat "$@" | 
      grep --line-buffered -E \
        "^([^|]*\|){13}20160920100643(\|[^|]*){7}\|567094398953(\||$)"
  ' sh | head)

並列アプローチでは、grepコマンドの出力が混在する可能性があることに注意してください (ただし、行バッファリングを使用し、行のサイズが数キロバイト未満である場合は、行の境界は保持されるはずです)。

答え2

@Stéphane Chazelasの回答では、コマンドパイプラインを最適化する方法について多くの詳細が提供されています。

find . -name "muc*_*_20160920_*.unl*" | xargs zcat |
    awk -F "|" '{if($14=="20160920100643" && $22=="567094398953") print $0}'| head

この問題に取り組む別の方法を紹介します。実際に最も多くの時間を費やしている場所を測定します。時間を費やしている場所がわかれば、それに対して何をすべきか判断できます。10 分間の実行時間を改善したい場合、2 秒かかるステップを最適化してもほとんど意味がありません。

コマンド パイプラインを見ると、次の 3 つの点に注目します。

  1. find .- ディレクトリ構造はどのようなものですか?ディレクトリごとにいくつのファイルがありますか?ディレクトリはコマンドが実行されるシステムのローカルディレクトリですか?リモートファイルシステムは多くもっとゆっくり。
  2. -name "muc*_*_20160920_*.unl*"- ディレクトリ構造内のすべてのファイル名はどれくらい近いですか?それらはすべて名前に「近い」ため、一致させるのが難しく、CPUを集中的に使用していますか?ディレクトリ ツリー内のファイルの名前をディスクから読み取り、パターンと比較する必要があります。
  3. xargs zcat- は、特に上記の問題や自体xargsと比較すると、パフォーマンス上の大きな問題にはならないと思います。ファイル名が10,000個、あるいは10,000,000個であっても、名前を渡して解析するだけの時間は、findzcat発見名前をつけて、ファイル自体を開いて解凍します。ファイルのサイズはどれくらいですか?ファイル全体を解凍するのでfindのファイル名パターンに一致するファイル。

主要なパフォーマンスの問題が何であるかをどのように判断すればよいでしょうか? パイプライン内の各コマンドのパフォーマンスを測定します。(https://stackoverflow.com/questions/13294554/how-to-use-gnu-time-with-pipelineパイプライン全体のタイミングの詳細については、以下を参照してください。次のコマンドを実行すると、各ステップがパイプライン全体の処理時間にどのくらいの時間を費やしているかを確認できます。

/usr/bin/time find .- これは、ディレクトリ ツリーの実行にかかる時間を示します。これが遅い場合は、より優れたストレージ システムが必要です。 ファイルシステムのキャッシュをフラッシュするこれを計測して最悪のケースの測定値を取得する前に、もう一度時間計測を実行してfind、キャッシュがパフォーマンスにどの程度影響するかを確認します。ディレクトリがローカルでない場合は、ファイルが存在する実際のシステムでコマンドを実行してみてください。

/usr/bin/time find . -name "muc*_*_20160920_*.unl*"- これにより、ファイル名のパターン マッチングにかかる​​時間がわかります。再度、ファイル システム キャッシュをフラッシュして 2 回実行します。

/usr/bin/time bash -c "find . -name 'muc*_*_20160920_*.unl*' | xargs zcat > /dev/null"- これが、パイプラインの実行時間が長くなっている主な原因であると思われます。これが問題である場合、zcatStéphane Chazelas の回答に従ってコマンドを並列化することが最善の解決策かもしれません。

最も多くの時間を費やしている箇所が見つかるまで、元のコマンド パイプラインからテスト対象のパイプラインにステップを追加し続けます。ここでも、ステップが原因だと思いますzcat。そうであれば、zcat@Stéphane Chazelas が投稿した並列化が役立つかもしれません。

並列化はzcat役に立たないかもしれない。傷つくパフォーマンスが低下し、処理速度が低下します。zcat一度に 1 つのプロセスのみを実行すると、IO はディスク シークを最小限に抑える適切なストリーミング パターンになります。複数のzcatプロセスを同時に実行すると、IO 操作が競合し、ディスク ヘッドがシークする必要があり、先読みの有効性が低下するため、実際に処理速度が低下する可能性があります。

このzcatステップがパフォーマンスの主なボトルネックであり、zcat一度に複数のプロセスを実行しても効果がないか、実際に速度が低下する場合は、パイプラインが IO バウンドであるため、より高速なストレージを使用して問題に対処する必要があります。

繰り返しになりますが、コマンド パイプラインを実行しているマシンのローカル ディレクトリでない場合は、ファイル システムが実際に存在するマシンで実行してみてください。

答え3

コメントにもあるようにグレップこのようなタスクにはより良い選択ですグロブスター**として使用できるオプションall path inside the directory except hidden

shopt -s globstar
zgrep -m 10 '^\([^|]*|\)\{13\}20160920100643|\([^|]*|\)\{7\}567094398953' ./**muc*_*_20160920_*.unl*
shopt -u globstar

答え4

指摘されているように、追加の詳細がなければ正しい答えを出すことは不可能です。

locate -0 -b -r '^muc.*_.*_20160920_.*.unl.*gz' | 
   xargs -0  zcat |
   awk -F "|" '$14=="20160920100643" && $22=="567094398953"'| head
  • 1:locate (使用可能な場合) は、**またはよりもはるかに高速ですfind。使用する正規表現を調整する必要があります...

  • 2と3: POのフィルター

@rudimeier が賢明に指摘したように、 の可用性と更新状態に関する問題がありますlocate。(たとえば、ほとんどの Linux マシンでは、locate は毎日更新されるため、今日作成されたファイルを見つけることができません)

それでも、locate が利用できる場合は、非常に印象的な速度向上が得られます。

time ...POがさまざまなソリューションを提供できれば興味深いでしょう

関連情報