ファイル、ストリーム、コマンドライン引数、シグナルあたりを扱えれば、どんな言語でもシェルコマンドを実装できる反面、シェルスクリプトはコマンドを起動しまくる(=プロセスをforkしまくる)ので、シェルコマンドを実装するための言語は、プロセス起動のオーバーヘッドはできるだけ小さいほうが望ましいです。
そこで、各言語の起動時間比較を探してみたところ、Debianのメンテナの方がやっているのを見つけました。
26の処理系(Pythonは2系と3系で-S
オプションありなしがそれぞれあるので計4つ)で、Hello Worldを出力するコマンドを1000回起動して、かかった時間の平均をとっているようです。
おそらく今まで見た中でいちばん豪華なHello Worldです。
bdrungさんはこれらの結果を3つのカテゴリにまとめています。
速い(1ms) | まあまあ(5ms) | 遅い(50ms) |
---|---|---|
Bash C C++ D (gdc) Go (go) Haskell Lua Pascal Perl Rust Shell (dash) |
CShell Python 2 (with -S) ZShell |
C# (mcs) Cython (Python 2) Cython3 (Python 3) PHP Python 2 Python 3 Python 3 (with -S) PyPy (Python 2) Ruby |
- RaspberryPi 3で動かすと、bdrungさんのパソコン(Intel(R) Core(TM) i5-2400S CPU @ 2.50GHz, プログラマの使ってるマシンとしては普通かちょっと遅いくらい)のだいたい10倍くらいかかる
- Pascalが0.08msで爆速ですね、C(0.26ms)より速い
- Go, Haskell, Rust などの最近流行りのコンパイラ言語はいずれも「速い」カテゴリにはいっている
- Ruby, Pythonなどが含まれる「遅い」カテゴリの言語は、RaspberryPi 3で実行すると500msec近く起動に時間がかかるようです
ここから言えることは、
- シェルコマンドを実装する言語として、RubyやPythonを選択してしまうと、RaspberryPiのようなボードコンピュータで動かすのがつらいケースがでてくるかもしれない
- 自社サービス向けのなんたらかんたらCLIみたいなのを実装するときは、クロスビルドのサポートが充実しているGoを選択しておくのが無難かもしれない
- GoやRustやHaskellと言いたかったのだけどRustとHaskellを触ったことがない
手元でも動かしてみる
せっかくなので手元(MacBook Pro 13-inch, 2016, Two Thunderbolt 3 ports)で動かしてみます。(というか動かしてみないとタイトル詐欺になってしまう)
macOSで動かしたかったのですが、Debian前提のようなコードだったので、とりあえず素直にDockerで動かしてみます。
forkしてDockerfileを追加したものを置いておきました: https://github.com/miminashi/startup-time
実行
docker build -t startup-time ./
docker run -it startup-time
結果
Run on: Intel(R) Core(TM) i5-6360U CPU @ 2.00GHz | Debian GNU/Linux 9 (stretch) | 2018-12-16
Pascal (fpc 3.0.0+dfsg-11+deb9u1): 0.10 ms
C (gcc 6.3.0): 0.50 ms
Go (go go1.7.4): 0.51 ms
Shell (dash 0.5.8): 0.53 ms
Rust (rustc 1.24.1): 0.82 ms
Lua 5.2.4: 0.91 ms
C++ (g++ 6.3.0): 1.11 ms
Haskell (ghc 8.0.1): 1.13 ms
Bash 4.4.12(1): 1.17 ms
D (gdc 6.3.0): 1.17 ms
Perl 5.24.1: 1.29 ms
OCaml (ocamlc 4.02.3): 1.53 ms
ZShell 5.3.1: 1.78 ms
Python-S 2.7.13: 3.72 ms
Cython (cython 0.25.2): 9.30 ms
Python 2.7.13: 9.51 ms
Python3-S 3.5.3: 10.93 ms
PHP 7.0.33-0+deb9u1: 11.82 ms
C# (mcs 4.6.2.0): 15.16 ms
Cython3 (cython3 0.25.2): 17.47 ms
Python3 3.5.3: 17.60 ms
PyPy 5.6.0: 31.58 ms
Ruby 2.3.3p222: 48.97 ms
Go_GCC (gccgo 6.3.0): 55.42 ms
Java (javac 1.8.0_181): 55.59 ms
Scala (scalac 2.11.8): 305.10 ms
CShell 20110502-2.2+b1: 649.89 ms
- 概ねbdrungさんの結果と同じ傾向でした
- CShell(csh)だけがbdrungさんの結果に比べて200倍くらい遅くなっている、何故・・・
ちなみに、結果のソートはこんな感じでやってます。(シェルスクリプトアドベントカレンダーだし一応シェルスクリプトを書いた)
head -n 1 results/2018-12-16_1.txt &&
cat results/2018-12-16_1.txt |
sed '1d' |
sed 's/^..* \([0-9][0-9]*\.[0-9][0-9]*\) ms$/\1,&/' |
sort -t, -k 1n |
sed 's/^[0-9][0-9]*\.[0-9][0-9]*,//'
sort -n
で小数をソートしてくれることを期待してますが、POSIX準拠の動作ではないので注意。
まとめ
- 起動の速さ以外にも、クロスビルドのサポートやワンバイナリ化など、シェルコマンドをつくるに際して便利な機能があるので、現代においてシェルコマンドはやはりgoで書くのが無難なのかもしれない
- というかgoの登場によって、作り置きしておいたシェルコマンドをシェルスクリプトでつなぎ合わせるという伝統的なUNIX Wayが再び現実的な選択肢になってきた
- Rustもcargoがクロスビルドをサポートしているようなので、goと同じような使い方ができそう
- 爆速のPascalや、Haskell, OCaml, Dなどもそのうち触ってみたい
- NodeJSがなかったけど、論外ということなのだろうか?追加してみよう
- きちんとベンチマークとったことはないけど、RaspberryPiでNodeJS動かすと起動に600msくらいかかっていた記憶がある
- mrubyなんかもワンバイナリにビルドできるので、追加して比較してみたい