最大パフォーマンスのための10の必須Bashスクリプティングのヒント
プロセスフォークの削減、組み込みコマンドの使用、ファイル操作のバッチ処理、適切なテキストツールの選択により、Bashスクリプトを高速化します。
最大パフォーマンスのための10の必須Bashスクリプティングのヒント
Bashスクリプトのパフォーマンスは、通常、シェルに一度に1つのプロセスでどれだけの作業を依頼するかにかかっています。10ファイルでは問題なく感じられるスクリプトも、5万ファイルをループし、アイテムごとにsed、grep、またはchmodを起動すると、耐え難いほど遅くなることがあります。
スクリプトが多くのファイルを処理したり、大きなテキスト出力を解析したり、小さな遅延が積み重なるほど頻繁に実行される場合に、これらのヒントを使用してください。
1. 外部コマンドの呼び出しを最小限にする
Bashが外部コマンド(例:grep、awk、sed)を実行するたびに、新しいプロセスをフォークし、これによりかなりのオーバーヘッドが発生します。スクリプトを高速化する最も効果的な方法は、可能な限りBashの組み込みコマンドを利用することです。
外部ユーティリティよりも組み込みコマンドを優先する
例: 条件チェックに外部のtestや[を使用する代わりに:
| 遅い(外部) | 速い(組み込み) |
|---|---|
if test -f "$FILE"; then |
if [[ -f "$FILE" ]]; then |
ヒント: 算術演算には、常に(( ... ))をexprやletの代わりに使用してください。算術式展開はシェル内部で処理されるためです。
# 遅い
COUNT=$(expr $COUNT + 1)
# 速い(組み込みの算術式展開)
(( COUNT++ ))
2. 効率的なループ構造を使用する
コマンド出力を反復処理する従来のforループは、プロセスの生成やワード分割の問題により遅くなる可能性があります。ネイティブのブレース展開やwhile readループを正しく使用してください。
for i in $(cat file) を避ける
$(cat file)を使用すると、ファイル全体を最初にメモリに読み込み、その後ワード分割の対象となるため、非効率的であり、ファイル名にスペースが含まれているとエラーが発生しやすくなります。代わりに行ごとの処理にはwhile readループを使用してください:
# ファイルを行ごとに処理する推奨方法
while IFS= read -r line;
do
echo "Processing: $line"
done < "data.txt"
IFS= read -r に関する注意: IFS=を設定すると、先頭/末尾の空白のトリミングが防止され、-rはバックスラッシュの解釈を防ぎ、データの整合性を確保します。
3. パラメータ展開でデータを内部処理する
Bashは、文字列に対して内部で動作する強力なパラメータ展開機能(部分文字列の削除、置換、大文字小文字変換など)を提供し、単純なタスクではsedやawkなどの外部ツールを避けることができます。
例:プレフィックスの削除
変数filenameからプレフィックスlog_を削除する必要がある場合:
filename="log_report_2023.txt"
# 遅い(外部のsed)
# new_name=$(echo "$filename" | sed 's/^log_//')
# 速い(組み込みの展開)
new_name=${filename#log_}
echo "$new_name" # 出力: report_2023.txt
4. 高コストなコマンド出力をキャッシュする
スクリプト内で同じ高コストなコマンド(例:APIの呼び出し、複雑なファイル検出)を複数回実行する場合は、毎回再実行する代わりに、結果を変数や一時ファイルにキャッシュしてください。
# 最初に一度だけ実行
GLOBAL_CONFIG=$(get_system_config_from_db)
# 以降の使用では変数を直接読み取る
if [[ "$GLOBAL_CONFIG" == *"DEBUG_MODE"* ]]; then
echo "Debug mode active."
fi
5. リストには配列変数を使用する
アイテムのリストを扱う場合は、スペース区切りの文字列ではなくBash配列を使用してください。配列はスペースを含むアイテムを正しく処理し、一般的に反復処理や操作においてより効率的です。
# 遅い/エラーが発生しやすい文字列リスト
# FILES="file A fileB.txt"
# 速くて堅牢な配列
FILES_ARRAY=( "file A" "fileB.txt" "another file" )
# 効率的な反復処理
for f in "${FILES_ARRAY[@]}"; do
process_file "$f"
done
6. 過剰なクォーティングとアンクォーティングを避ける
適切なクォーティングは正確性にとって重要ですが(特にスペースを含むファイル名を扱う場合)、過剰なクォーティングとアンクォーティングはわずかなオーバーヘッドを追加する可能性があります。より重要なのは、クォートが必須である場合とオプションである場合を理解することです。
算術式展開((...))の場合、コマンド置換$()とは異なり、式自体の周りにクォートは一般的に必要ありません。
7. 可能な場合はパイプラインにプロセス置換を使用する
プロセス置換(<(cmd))は、名前付きパイプ(mkfifo)よりもクリーンで高速なパイプラインを作成できる場合があります。特に、1つのコマンドの出力を別のコマンドの2つの異なる部分に同時に供給する必要がある場合に便利です。
# 2つのソート済みファイルの内容を効率的に比較
if cmp <(sort file1.txt) <(sort file2.txt); then
echo "Files are identical when sorted."
fi
8. echo の代わりに printf を使用する
多くの場合無視できますが、echoの動作はシェルやシステムによって異なり、バックスラッシュの解釈に複雑な処理が必要になることがあります。printfは一貫したフォーマットと優れた制御を提供し、大量の出力操作において一般的により信頼性が高く、場合によってはわずかに高速です。
# 一貫した出力
printf "User %s logged in at %s\n" "$USER" "$(date +%T)"
9. find ... -exec ... {} + を -exec ... {} ; よりも優先する
findコマンドを使用して、見つかったファイルに対して別のプログラムを実行する場合、セミコロン(;)で終了するかプラス記号(+)で終了するかの違いは、パフォーマンスに大きな影響を与えます。
{}; は、コマンドをファイルごとに1回実行します。(オーバーヘッド大){}+ は、可能な限り多くの引数をまとめて、コマンドを1回実行します(xargsのように)。(オーバーヘッド小)
# 遅い:'chmod 644' を数千回実行
find . -name '*.txt' -exec chmod 644 {} \;
# 速い:'chmod 644' を1回または数回、多くの引数で実行
find . -name '*.txt' -exec chmod 644 {} +
10. 重いテキスト処理には awk または perl を使用する
目標は外部呼び出しを最小限にすることですが、重く複雑なテキスト操作が必要な場合、awkやperlのような専門的なツールは、複数のgrep、sed、cutコマンドをチェーンするよりもはるかに高速です。これらのツールはデータを1回のパスで処理します。
cat file | grep X | sed Y | awk Z と書いていることに気付いたら、これを1つの最適化されたawkスクリプトに統合してください。
実用的なルール
高速なBashスクリプトは、Bashループ内で行う作業を減らします。単純なテストや文字列編集にはシェルの組み込みコマンドを使用し、find -exec ... {} + や xargs でファイル操作をバッチ処理し、テキスト処理が主要な作業になったら awk、perl、または他の本格的なパーサーに切り替えてください。
最適化する前に、time やシェルトレースを使用して代表的な実行を測定してください。その後、最も多くのコマンドを生成するループを修正してください。