ブログ記事

シェルスクリプト高速化テクニック - 処理時間を1/10にする方法

「このスクリプト、なんでこんなに遅いの...」大量のファイル処理で悩んでいませんか?パイプライン最適化、並列処理、そして意外と知られていない高速化テクニックを実例で解説します!

Tips
shell bash performance optimization linux
シェルスクリプト高速化テクニック - 処理時間を1/10にする方法のヒーロー画像

シェルスクリプトを書いていて「なんか遅いな…」と感じたこと、ありませんか?

私は以前、10 万個のログファイルからエラーを抽出するスクリプトを書いたとき、処理に 30 分以上かかって頭を抱えました。でも、ちょっとした工夫で、なんと 2 分まで短縮できたんです!

今日は、そんな経験から学んだシェルスクリプトの高速化テクニックを共有します。

まずは計測から始めよう

高速化の第一歩は「どこが遅いのか」を知ること。私がよく使う計測方法を紹介します:

# 時間計測の基本
time ./your_script.sh

# より詳細な計測
#!/bin/bash
start_time=$(date +%s.%N)

# ここに処理を書く

end_time=$(date +%s.%N)
elapsed=$(echo "$end_time - $start_time" | bc)
echo "処理時間: ${elapsed}秒"

1. ループを避ける(最重要!)

これが一番効果的です。シェルスクリプトのループは想像以上に遅い!

❌ 悪い例:whileループで1つずつ処理

# 10万ファイルで約10分かかる処理
find . -name "*.log" | while read file; do
    grep "ERROR" "$file" >> errors.txt
done

✅ 良い例:xargsで並列処理

# 同じ処理が約30秒で完了!
find . -name "*.log" -print0 | \
    xargs -0 -P 8 grep -h "ERROR" >> errors.txt

-P 8 で 8 プロセス並列実行。CPU コア数に合わせて調整してください。

2. 外部コマンドの呼び出しを減らす

シェルスクリプトでは、外部コマンドの起動コストが意外と大きいんです。

❌ 悪い例:ループ内でsedを何度も呼ぶ

# ファイルごとにsedを起動(遅い!)
for file in *.csv; do
    sed 's/,/\t/g' "$file" > "${file%.csv}.tsv"
done

✅ 良い例:awkで一括処理

# awkで全ファイルを一度に処理
awk 'BEGIN{OFS="\t"} {gsub(/,/, "\t"); print > (FILENAME ".tsv")}' *.csv

3. パイプラインの最適化

パイプラインの順番を変えるだけで、劇的に速くなることがあります。

❌ 悪い例:全データを通してから絞り込む

# 巨大なファイルを全部読んでから絞り込み
cat huge_file.log | grep "ERROR" | awk '{print $1, $3}' | sort | uniq

✅ 良い例:早めに絞り込む

# grepで先に絞り込んでデータ量を減らす
grep "ERROR" huge_file.log | awk '{print $1, $3}' | sort -u

さらに、sort -usort | uniq より高速です!

4. プロセス置換の活用

一時ファイルを作らずに複数の処理を並列実行できます。

# 一時ファイルを使う場合(遅い)
grep "pattern1" bigfile > temp1.txt
grep "pattern2" bigfile > temp2.txt
diff temp1.txt temp2.txt
rm temp1.txt temp2.txt

# プロセス置換を使う場合(速い!)
diff <(grep "pattern1" bigfile) <(grep "pattern2" bigfile)

5. 組み込みコマンドを使う

Bash の組み込みコマンドは外部コマンドより圧倒的に高速です。

文字列操作の例

# 外部コマンド(遅い)
filename=$(echo "$path" | sed 's/.*\///')
extension=$(echo "$filename" | sed 's/.*\.//')

# Bashの組み込み機能(速い!)
filename="${path##*/}"
extension="${filename##*.}"

よく使うBashの文字列操作

# パス操作
path="/home/user/document.pdf"
dir="${path%/*}"        # /home/user
file="${path##*/}"       # document.pdf
name="${file%.*}"        # document
ext="${file##*.}"        # pdf

# 置換
text="hello world"
echo "${text/world/universe}"  # hello universe
echo "${text//l/L}"           # heLLo worLd

6. 並列処理の極意

GNU Parallel を使えば、もっと簡単に並列化できます:

# インストール
# Ubuntu/Debian: sudo apt install parallel
# Mac: brew install parallel

# 基本的な使い方
find . -name "*.jpg" | parallel -j 8 convert {} {.}_thumb.jpg

# プログレスバー付き
ls *.mp4 | parallel --progress -j 4 ffmpeg -i {} -c:v libx264 {.}_compressed.mp4

7. 実践例:ログ解析の高速化

実際のログ解析スクリプトを最適化してみましょう。

Before(処理時間:約15分)

#!/bin/bash
# slow_log_analyzer.sh

for logfile in /var/log/app/*.log; do
    echo "Processing $logfile..."

    # エラーを抽出
    grep "ERROR" "$logfile" > temp_errors.txt

    # IPアドレスを抽出してカウント
    while read line; do
        ip=$(echo "$line" | grep -oE '[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}')
        echo "$ip" >> all_ips.txt
    done < temp_errors.txt
done

# 集計
sort all_ips.txt | uniq -c | sort -nr > ip_counts.txt
rm temp_errors.txt all_ips.txt

After(処理時間:約45秒)

#!/bin/bash
# fast_log_analyzer.sh

# 並列処理でエラー行からIPを抽出して集計
find /var/log/app -name "*.log" -print0 | \
    xargs -0 -P $(nproc) grep -h "ERROR" | \
    grep -oE '[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}' | \
    sort | uniq -c | sort -nr > ip_counts.txt

なんと 20 倍の高速化!ポイントは:

  • ループを使わない
  • 並列処理(nproc で CPU コア数を自動取得)
  • パイプラインで一気に処理
  • 一時ファイルを作らない

8. メモリ効率も考える

大量のデータを扱うときは、メモリ使用量も重要です。

# メモリを大量に使う(全データをソート)
cat huge_file | sort | uniq > result.txt

# メモリ効率が良い(部分的にソート)
sort -S 1G huge_file | uniq > result.txt  # 1GBのメモリ制限

# さらに効率的(外部ソート)
LC_ALL=C sort huge_file | uniq > result.txt  # ロケール無効化で高速化

9. デバッグとプロファイリング

スクリプトのボトルネックを見つける方法:

#!/bin/bash
# debug_profile.sh

# デバッグモードON
set -x  # 実行するコマンドを表示
PS4='+ $(date "+%s.%N") ${BASH_SOURCE}:${LINENO}: '  # タイムスタンプ付き

# ここに処理を書く
find . -name "*.txt" -print0 | xargs -0 grep "pattern"

# デバッグモードOFF
set +x

高速化チェックリスト

最後に、私が使っている高速化チェックリストを共有します:

  1. ✅ ループの代わりに xargsparallel を使える?
  2. ✅ 外部コマンドの呼び出し回数を減らせる?
  3. ✅ パイプラインの順番は最適?
  4. ✅ 一時ファイルの代わりにプロセス置換を使える?
  5. ✅ Bash の組み込み機能で代替できる?
  6. ✅ 並列処理できる部分はない?
  7. sortgrep に LC_ALL=C を付けた?(日本語不要なら)

まとめ

シェルスクリプトの高速化は、ちょっとした工夫の積み重ねです。特に「ループを避ける」「並列処理する」の 2 つを意識するだけで、劇的に速くなることが多いです。

私の経験上、これらのテクニックを組み合わせれば、ほとんどのスクリプトで 5〜10 倍の高速化が可能です。30 分かかっていた処理が 3 分で終わるようになったときの爽快感…ぜひ体験してください!

次回は「開発者のための時短コマンド集」を紹介します。ripgrep や fd など、最新の CLI ツールで開発効率を爆上げしましょう!

この記事は役に立ちましたか?

Daily Hackでは、開発者の皆様に役立つ情報を毎日発信しています。