bashの組込みコマンド自作によるスクリプトの高速化

  • 272
    Like
  • 0
    Comment

はじめに

bashには次の2つの理由によって、組み込みコマンド(builtin command)というものが存在します。

  • スクリプトの高速化のため。組み込みコマンドであれば通常のコマンドを実行する場合に比べてプロセスの生成コスト(fork()/exec())が削減できる
  • bash自身の状態を変更させるため。例えばcdコマンドを/bin/cdとして用意してbashから当該コマンドを実行しても、当該コマンドのpwdが変更されるだけで、bashのそれは変更されないため、意味がない

今回は前者に焦点を合わせて、その効果と、組み込みコマンドの自作方法について述べます。

予備知識: 組込みコマンドによるスクリプト高速化の効果

組込みコマンドそのものの存在、及びその存在意義について既にご存知のかたは、この節を飛ばしてもらって構いません。

例えば皆さんがbashスクリプトからechoコマンドを実行した場合、通常は/bin/echoコマンドではなくbashの組込みコマンドechoを実行します。目的は前述の通り、高速化です。

組込みコマンドによる高速化の効果を体験してみましょう。改行のみを出力する/bin/echoコマンドを一万回実行したときの経過時間を計測した結果を示します。出力そのものは不要なので/dev/nullに捨てています。

$ time (for ((i=0;i<10000;i++)) ; do /bin/echo >/dev/null; done)

real    0m5.760s
user    0m1.744s
sys     0m1.084s
$ 

所要時間はおよそ5.8秒でした。

同じことをbash組み込みコマンドで、つまり通常みなさんがbashから実行するechoでやってみましょう。

$ time (for ((i=0;i<10000;i++)) ; do echo >/dev/null; done)

real    0m0.086s
user    0m0.072s
sys     0m0.012s
$ 

こちらの所要時間はおよそ0.086秒と、通常のコマンドを実行する場合に比べて約1.4%の時間で処理を終えられました。bashはこのようにして様々な頻出コマンドを組み込みコマンド化によって高速化しています。

次のようにすれば組み込みコマンドのリストを得られます。興味のあるかたはご確認ください。

$ enable 
enable .
enable :
enable [
...
enable unalias
enable unset
enable wait
$ 

それぞれ高速化のためのものか、あるいは組込みコマンドでなければ実現不可能な機能なのかを考えてみるのもおもしろいかもしれません。

コマンド作成の実行環境

本記事執筆時点でのdebian/testing(stretch)最新版

必要なパッケージ

bash-builtins

組み込みコマンドの作成

簡単のため、みなさんがbashスクリプトを書いたとして、その中で頻繁に呼ばれるhello worldプログラムが性能ボトルネックになっていると仮定しましょう。この問題を、同コマンドの組み込みコマンド自作によって解決します。

もとのコマンドのソースは次の通りです。

hello.c
#include <stdio.h>

int main(void) {
    puts("Hello world!");
    return 0;
}

実行してみましょう。

$ ./hello 
Hello world!
$ 

これと同じことをする組み込みコマンドのソースは次の通りです。

myhello.c
#include <builtins.h>
#include <shell.h>
#include <stdio.h>

static int myhello(WORD_LIST *list) {
    puts("Hello world!");
    fflush(stdout);
    return EXECUTION_SUCCESS;
}

static char *desc[] = {
    "Show a greeting message.",
    "",
    "It's far faster than launching executable file",
    "because it't not necessary to call exec() and fork().",
    (char *)NULL
};

struct builtin myhello_struct = {
    "myhello",      // builtin command name
    myhello,        // function called when issueing this command
    BUILTIN_ENABLED,    // initial flag
    desc,           // long description
    "myhello",      // short description
    0,
};

組込みコマンドの処理に対応する関数myhello(), ドキュメントとなるdesc変数、および、この組み込みコマンドをbashに登録するために必要なmyhello_structを作成する必要があります。それぞれの意味についてはソース内のコメントや後述の参考資料をごらんください。

これをビルドするためのMakefileは次のようになります。

BINARIES := hello myhello

.PHONY: all clean

all: hello myhello

myhello: myhello.c
    cc -L $@ -I/usr/include/bash/ -I/usr/include/bash/include -fpic -shared -o myhello.so myhello.c

clean:
    rm -rf *.o *.so *~ $(BINARIES)

ビルドしましょう。

$ make
cc     hello.c   -o hello
cc -L myhello -I/usr/include/bash/ -I/usr/include/bash/include -fpic -shared -o\
 myhello.so myhello.c
$ ls
LICENSE  Makefile  README.md  benchmark  hello  hello.c  myhello.c  myhello.so

成功です。作成されたmyhello.soという共有ライブラリが組み込みコマンドの実体です。

作成したコマンドの組み込み

コマンドの組み込みは次のようにします。

$ enable -f ./myhello.so myhello
$ 

これは./myhello.soという共有ライブラリをmyhelloという名前で組み込むという意味です。共有ライブラリ名の前の"./"を省略するとコマンドが失敗するので注意してください。

成功したかどうかを確認しましょう。

$ enable | grep myhello
enable myhello
$ 

myhello組み込みコマンドが正しくbashに認識されています。

ドキュメントも表示できます。

$ help myhello
myhello: myhello
    Show a greeting message.

    It's far faster than launching executable file
    because it't not necessary to call exec() and fork().
$ 

実行してみましょう。

$ myhello
Hello world!
$ 

成功です。

効果の確認

それぞれ同じことをする実行ファイル(./hello)とmyhello組み込みコマンドを10000回連続実行した際の所要時間を計測してみましょう。

$ time (for ((i=0;i<10000;i++)) ; do ./hello >/dev/null ; done)

real    0m5.508s
user    0m1.932s
sys     0m0.848s
$ time (for ((i=0;i<10000;i++)) ; do myhello >/dev/null ; done)

real    0m0.087s
user    0m0.076s
sys     0m0.008s
$ 

組み込みコマンドは実行ファイルの場合に比べて1.5%の所要時間で処理を終えられました。

注意

組み込みコマンドの自作は技術的には面白く、かつ、うまく使った場合の効果は高いのですが、次の理由によって、実際に適用できる範囲は少ないです。

  • Cソースを書いてbash組み込みコマンド専用の共有ライブラリをビルドしなくてはいけないため、移植性が低い
  • 組込みコマンド化によって削減できるのはfork()/exec()のコストだけなので、前述のような著しい高速化が期待できるのは実行時間が極めて短いコマンドが大量に呼ばれる場合のみ1

この機能は乱用を避けて、特定環境で特定処理だけを高速化したいというような特殊な場合にのみ使用するのがよいと筆者は考えます。ちょうどRubyやPythonなどのスクリプト言語の一部をCで実装したり、Cプログラムの一部をアセンブリ言語で実装したりするのと似ています。

おわりに

本記事で使用したソースはgithub上にアップロードしていますので、参考にしてください。

参考資料


  1. 例えばfork()/exec()のコストが0.1秒だとして、実行に10秒を要するCプログラムを組み込みコマンドに置き換えたとしても削減できる実行時間は1%程度