ShellScript
AdventCalendar
初心者向け
シェルスクリプト

続: シェルスクリプトを何万倍も遅くしないためには —— やはりパイプは速いし解りやすい

外部コマンドを使わなければ大丈夫?

この記事では、先日の投稿『シェルスクリプトを何万倍も遅くしないためには —— ループせずフィルタしよう』の補足として、いただいた反応の一つにお答えしたいと思います。

悪記事。正しくは「プロセスの起動はコストが高いのでループごとにプロセスを起動するのは避けましょう」です。外部コマンドを起動しないならシェルスクリプトで一行ごとに読み込むことは何の問題もないです。
—— megumin1のコメント2017/12/01 23:54

「プロセスの起動はコストが高いのでループごとにプロセスを起動するのは避けましょう」というのはその通りだと思います。
まさにそのことを説明しようとした記事だったのですが、私の筆力が到りませんでした。

では、「外部コマンドを起動しないならシェルスクリプトで一行ごとに読み込むことは何の問題もない」というのは正しいでしょうか。
外部コマンドの起動が遅い、という問題に対しては、2つの対応が考えられます。

  • パイプ: コマンドをパイプでつなげ、それぞれの起動を1回ずつに抑える
  • 内部機能: 処理を全てシェルの内部機能で行ない、外部コマンドの起動を0回にする

先日の記事では前者のパイプを使用しましたが、「シェルスクリプトで一行ごとに読み込むことは何の問題もない」とすると後者でもよさそうです。
いやむしろ、外部コマンドの起動が0回ですから、こちらのほうが速いのでしょうか。

計測してみます。
まず前回の確認ですが、ループ毎に外部コマンドを起動する悪い例と、パイプを用いたものの差は数万倍でした。

chart_01.png

では、内部機能だけ用いたものは、これらをどれだけ改善するでしょうか。
内部機能バージョンは、前回コメントいただいた、Bash拡張を利用するものをそのまま使用させていただきます。

chart_02.png

悪い例より60倍速くなりました。
しかし、パイプに比べると400倍近く遅いです。
速いシェル、Kshで試してみます。

chart_03.png

さらに4倍ほど速くなりましたが、どうもパイプには及びません。
Bash拡張で文字列の置換などをしているのがよくないのかも知れませんね。
では、ループの中で何も行なわないことにしてみましょう。

while IFS=, read -r x target x; do
    :
done

: は内部コマンドです)

chart_04.png

だいぶ速くなりました。
しかし、この read するだけのスクリプトにしても、パイプに比べると圧倒的に遅い結果です。

外部コマンドを起動しないならシェルスクリプトで一行ごとに読み込むことは何の問題もない

とは言えそうにありません。

シェルは外部コマンドを前提に作られている

使っていると、シェルはフィルタをパイプでつなげることを前提としていることが解ります。
内部機能しか使わないスクリプトをより速く実行するシェルというのも、作成することはできるでしょうが、そうするとシェル自体の起動速度も落ちるでしょうし、今のところあまり需要が無さそうです。

フィルタの使用は最適化のための難解なテクニックではない

もちろん、数百倍であれ、数万倍であれ、遅さが問題にならないことも多いです。
そういった場合に無理にループを避ける必要はありません。
しかし、ループを使わずにフィルタをパイプでつなげることは、速度の問題さえ無ければ避けたいような難しいテクニックなのでしょうか?

フィルタをパイプでつなげるバージョン
#!/bin/sh
cut -d, -f2 |tr -dc 'aeiou' |wc -c

コマンドの意味を知っていれば、上のコードは左から順に理解していくことができます。

  • cut(列を切り出す) -d,(,区切りで) -f2(2列目だけを)
  • tr(置き換える) -d(無に) -c 'aeiou'(aeiou以外を)
  • wc(数える) -c(バイト数を)

本来、こういったシェルスクリプトは「最適化のために頑張って書かなければならない難解なコード」といったものではなく、それなりに解りやすいものだと思います。
もちろん好みは様々ですし、パズルのように頭を使わなければならないこともありますが、シェルスクリプトを書く際は、他の言語の習慣にこだわらず、フィルタを多用してみてください。

この記事のライセンス

クリエイティブ・コモンズ・ライセンス
この記事はCC BY-SA 4.0(クリエイティブ・コモンズ 表示 4.0 継承 国際 ライセンス)の元で公開します。