mrubyビルド設定項目解説

この投稿はmruby Advent Calendar 2013の24日目の記事です。
23日目はyamanekko さんのmrubyをRaspberry Pi(bare metal)で動かす: Windows編、25日目はyukihiro_matzさんの24時間一人mrubyハッカソンです。

はじめに

mrubyはカスタム性の高さも売りの一つで、/include/mrbconf.hを弄ることである程度動作を変えられるようになっています。
しかし、設定の多さに圧倒され、すべてデフォルトのまま使っているという人も多いのではないでしょうか?
そこで今回はわかる範囲で各種設定について説明してみようかと思います。

mrubyの設定(特にメモリーレイアウトに関する部分)は環境により動作が異なることが多々あります。
今回は筆者の開発環境であるVisualStudio2013にて試したものであり、gccやclangや特定環境専用のコンパイラなどでは結果が異なる可能性があることを念頭に置いてお読みください。

2013/11/25当時(git hash: f5bd87b9e6d0d8a84cf866b4847c1416e4f5c622 )の物です。
それ以降のmrubyを使用する場合は以下の解説の通りではない可能性があります。

2013/12/24 23:30追記
MRB_GC_TURN_OFF_GENERATIONALとMRB_GC_FIXED_ARENAの説明が間違っていたため、関連記述を変更しました。
また、間違いの指摘をしてくださったMatzさん、どうもありがとうございます。

MRB_USE_FLOAT

実数を扱う型をdoubleからfloatに変更します。
VS2013でデフォルト設定であればmrb_valueのサイズは16byteですが、これを定義すると8byteになります。
しかし当然ですがfloatになるので精度がかなり落ちます。
実数なんか使わないからメモリー消費量抑えたい場合などに使うといいでしょう。

MRB_INT16

整数を扱う型をint32_tからint16_tに変更します。
しかし、ポインターなどを扱う都合からmrb_valueのサイズが縮まったりはしません。
おそらくは16bit CPUなど2byte演算が常用される環境用なのだと思われます。

MRB_INT64

整数を扱う型をint32_tからint64_tに変更します。
MRB_INT16と同時には使えません。
デフォルトの設定(MRB_USE_FLOATやMRB_XX_BOXINGが無効の環境)では特にmrb_valueのサイズが増えたりはしないので、32bit範囲を超える演算を多用するなら有効にするのもよいでしょう。

MRB_NAN_BOXING

実数の特殊状態であるNaNの定義の一部を利用して値を投入することでメモリー消費量を落とす機能を有効にします。
MRB_USE_FLOAT、MRB_INT64とは同時には使えません。
一般的な環境であればこれが有効になるとmrb_valueのサイズが16byteから8byteになります。
ただし実数演算の実装依存なため、環境により動かない可能性があります。

MRB_ENDIAN_BIG

一部構造体のメンバーの配置順を変更します。
名前から誤解されがちですが、整数を格納するエンディアンがビッグエンディアンになったりはしません。
メンバーのアドレスが変わるぐらいしか変化がないので、通常の環境であればON/OFFどちらでも特に影響はない設定です。

MRB_WORD_BOXING

ポインターは下位2bitは使われないことと、整数の1bitを犠牲にすることでメモリー消費量を落とす機能を有効にします。
MRB_NAN_BOXINGと同時には使えません。
mrb_valueは4byteになりますが実数がポインターで保持されるため、実数を多用すると逆にメモリー消費量が増えてしまいます。
VS2013ではバグなのか私が知らない仕様なのか、MRB_INT16かMRB_INT64と共に定義すると一部ビットフィールドが無視されサイズが増えてしまいます。

MRB_FUNCALL_ARGC_MAX

mrb_funcall関数で渡せる引数の最大数を設定できます。
mrb_funcall関数はここで設定した分の長さを持つmrb_valueの配列をスタックに確保するので、スタックに余裕がない環境の場合は小さく設定するとよいでしょう。
(といっても配列一つなので大して変わりませんが)
ちなみに、ここで設定された以上の引数を要求するメソッドもmrb_funcall_argvを使えば呼ぶことができます。
なので、mrbgemなどどういう設定で使用されるかわからないものを実装する場合、mrb_funcall_argvを使ったほうがいいかもしれません。

MRB_HEAP_PAGE_SIZE

インスタンスなどに使用するヒープ領域の1確保単位を設定できます。
1確保単位と書きましたが、単位はbyteではなくインスタンスで、1024なら新たなヒープを確保せずに1024インスタンスを割り当てることができます。
またヒープを使用するのはあくまでインスタンスそのものだけで、arrayの本体である動的確保部分などは別途mrb_mallocで確保されます。

MRB_USE_IV_SEGLIST

インスタンス変数を保持するデータ構造をハッシュからリンクリスト形式に変更します。
インスタンス変数が少なければハッシュ計算が不要な分高速になるかもしれません。
ちなみにインスタンス変数一つにつき1要素ではなく、(執筆時点では)4つのインスタンス変数を保持できる配列が1要素のリストになります。

MRB_IVHASH_INIT_SIZE

インスタンス変数を保持するハッシュテーブルの初期サイズを設定します。
MRB_USE_IV_SEGLISTが定義されている場合は無視されます。

MRB_IREP_ARRAY_INIT_SIZE

何に使われているか不明です。
というより、grepしても出てこなかったので今はもう使われていないのではないかと思われます。
2013/12/24 23:30追記
hash: 9fab01caでmruby本体から削除されたようです。

MRB_GC_TURN_OFF_GENERATIONAL

世代別GCを無効にします(有効ではなく無効な点に注意)。
定義すると単なる3色インクリメンタルマーク&スイープになります。
逆にコメントアウトすると、追加で古いオブジェクトは暗黙でマークしてGCする処理が加わるようです。
GCの性能比較はよくわからないので割愛。

KHASH_DEFAULT_SIZE

Hashのデフォルトの初期サイズを設定します。
Hashクラスだけではなくmruby内部で使われるハッシュテーブル(名前空間の解決用など)にも適用されるようです。

POOL_ALIGNMENT

一括解放や確保できるメモリー空間として使用される独自のメモリープールにおいて、メモリー確保時に先頭アドレスをアラインするサイズを設定します。
メモリープールは主にパース結果など内部表現を保持するために使用されています。
アラインされないと速度が低下したり、そもそも読み書きに失敗するような環境において調整することを目的としているようです。

POOL_PAGE_SIZE

メモリープールの1確保単位を設定します。
メモリープールの容量が不足するたび、このサイズ分新たに確保して動作します。
当然ですが、大きくすれば処理速度が上がり、小さくすればメモリー効率が上がります。

MRB_STR_BUF_MIN_SIZE

文字列構築用バッファ確保(具体的にはmrb_str_buf_new関数)でメモリーを確保する際の最小サイズを設定します。
mrubyで扱う文字列全般というわけではなく、あくまでCレイヤーでなんらかの文字列を構築する際のバッファにだけ影響する様子。
メモリー再確保のコストを減らすためか、設定値以下のサイズを取得しようとすると自動的に設定値まで広げて確保されるようです。
mruby本体に限ればすべての使用箇所で適切にバッファサイズを指定しているので、いっそ小さな値にするとメモリー効率は上がるかもしれません。
(大して使用されていないので微々たるものでしょうが)

MRB_GC_ARENA_SIZE

Cレイヤーで処理する際の一時オブジェクト保存域であるarenaの初期サイズを指定します。
あくまで初期サイズであり、溢れるたびに1.5倍されていきます。
よほど特殊な例を除き、ちゃんとした実装であれば100を溢れることは滅多にありません。
自身のコード由来で大きくすることを検討している場合、下記のURLを参照のうえよく考えてから行うようにしてください。
arenaの詳細は http://www.rubyist.net/~matz/20130731.html を参照のこと。

MRB_GC_FIXED_ARENA

arenaのサイズを固定にします。
その場合のサイズはMRB_GC_ARENA_SIZEになります。
固定のメリットは若干の処理速度の向上と、arenaを大量に使うコードでクラッシュするため早期発見ができることです。
省メモリーが必須な環境なら、事前に固定に変更したうえで負荷テストなどを行うとarena溢れを防止できて効率的かもしれません。

DISABLE_STDIO

エラー時の出力などコンソールへの入出力を行わないようにします。
標準入出力先が存在しない環境など用と思われます。

ENABLE_DEBUG

デバッグ機能を有効にします。
有効だとmrb_stateにcode_fetch_hookというメンバーが増え、そこに関数ポインターを代入することで、1命令実行するごとにコールバックされるようになります。
当然ですがこれが有効のままだと処理速度は低下するので、有効のままリリースするのは推奨されません。

終わりに

いかがだったでしょうか。
書いてる側からすると、ヒープやプールなど独自に管理するメモリー領域が使われていたり、ON/OFFできる機能が意外と多かったりなど、知らなかったことが意外と多くて勉強になりました。
これを元にいろいろもっと気軽にmrbconfをカスタマイズするユーザーが増えてくれたら幸いです。

ではまた。

mrubyの小ネタ集

前回の関連で得た知見をそのまま忘れるに任せるのもちょっと惜しいと思ったので、小さく小ネタ集として残しておきます。

ローカル変数は最大約512個

mrubyのバイトコード上レジスタは最大でも512個しかありません。
そしてローカル変数1つにつき1つレジスタが割り振られるため、ローカル変数は最大で512個しか使えません。
さらに、メソッド呼び出しなど各種処理でもレジスタを使うので、前後のコード次第で事実上はもう少し少ない数になります。
足りなくなるということはないでしょうが、そういう仕組みなのだということは覚えておくと役に立つこともあるかもしれません。

リテラルの文字表現は最大1023文字

整数実数文字列といったリテラルを文字で表記するとき、最大文字数は1023文字となります。
文字列の場合は””を含まない本体の文字数、8進数は先頭の0もしくは0_を除いた文字数、16進数も同じく先頭の0xを除いた文字数が1023までなら使用できます。

少し前まではMRB_PARSER_BUF_SIZEという設定をいじれば変更できたのですが、私がしたバグ報告の結果、コンフィグ上から削除され変更は推奨されなくなりました。
一応/include/mruby/compile.hにまだ定義されているので、興味のある人は最大65535の範囲でいじってみましょう。

リテラルは使用した回数だけ定義される

短い文字列は毎回書いてしまうこともありますが、mrubyは11/30現在までのところ同一のリテラルをまとめる機能はありません。
実際同じ文字リテラルを複数使うコードをmrbcしたものをバイナリエディタで覗くと同じ文字列が複数個ならんでいるのが確認できます。

メモリー効率を考えるなら複数回同じリテラルは使わず、一度変数に代入し使いまわすようにするといいようです。

post引数という定義がある

post引数という定義の引数があります。
具体的には

def func(normal, *rest, post)

のpostという引数が相当します。

これは可変長引数の後の通常引数で、解釈は先頭から通常引数、末尾からpost引数と確定させていき、残りを可変長引数と解釈するようです。

mrubyの小ネタとして書きましたが、普通にrubyにもあります。

通常引数・オプション引数・post引数の数は31個まで

通常の引数の数は31個までしか受け取れません。
32個以上受け取るコードを書いた場合、特にシンタックスエラーにはなりませんがバイトコードは壊れます。
同様にオプション引数、post引数も31個しか定義できません。

合計が31個ではなく各引数が31個なので、組み合わせれば90個を超える引数でも可変長引数を使わずに受け取れます。

ローカル変数の名前はコンパイル時に破棄されている

2014/05/17追記:公式でローカル変数情報が実装されました、今ではKernel#local_variablesも動作するようになっています。

rubyであればKernel#local_variablesでローカル変数名の一覧が取得できますが、mrubyの場合コンパイル時にローカル変数名はすべて破棄されています。
ではどう管理されているかというと、変数一つにつきレジスタが一つ割り振られ、レジスタの1番地などという形で扱われます。
ちなみに、当然のことですがグローバル変数やインスタンス変数などはちゃんと変数名はシンボルとして処理されています。

終わりに

いかがだったでしょうか。
主にバイトコード内の定義上の限界から攻めていったので、最大数がどーたらというのが多かった気がします。
一応全部実際にコードを書いて試したつもりですが、何かミスなどあればコメントくだされば直しておきます。

ではまた。

mrubyにバグ報告をした@その2

mrubyを弄る過程でいろいろバグを発見して報告したので一応書くだけ書いておきます。

MRB_NAN_BOXING次第でコンパイル結果が変わる

MRB_NAN_BOXINGという設定のON/OFF次第でコンパイルしたバイトコードが変わる=>別の環境にもっていっても動かない、というバグ。

そもそもMRB_NAN_BOXINGとはは何かというと、doubleやfloatのNaN表現に余裕があることを利用し、そこに値を入れることでmrb_valueのサイズを減らそうというもの。

なぜそれでバグが起こるかというと、mrb_vtypeというmrubyの型情報をMRB_NAN_BOXINGで入れようとしたのだが、NaNの定義上の都合から0だけは入らず、すべて+1したものを入れるようにしていました。
それをそのままバイトコード側にも反映したため、MRB_NAN_BOXING次第で型のインデックスが1ずれ、動作しなくなっていたわけです。

バグ報告をしたところ、irep_pool_typeというバイトコード内に入れる専用の型情報が用意され、そちらから実行時に変換する形で修正されました。

実際に修正された時のdiff
https://github.com/mruby/mruby/commit/71354b91cb48ff3a5c1e3c09492d18a3c30efcb9

MRB_PARSER_BUF_SIZEが65536以上でバイトコードが壊れる

MRB_PARSER_BUF_SIZEが65536以上で65536文字以上のリテラルを使おうとするとバイトコードが壊れて実行できなくなるバグ。
あくまでバイトコードのロードに失敗するだけなので、ソースコードから直に実行する場合は問題ない。

まずMRB_PARSER_BUF_SIZEが何かから説明すると、パーサーで扱う最大のリテラル長を定義したもので、子の文字数以上のリテラルはパース時にエラーとされる。

で、なぜ65536文字以上だとだめなのかは簡単で、バイトコード上ではリテラルの長さに2byteしか割り当てていないため。

コンパイル時にエラーするべきでは?と報告したところ、そもそも65536以上にすることがないと判断されたのか、設定項目から削除され、1024固定となりました。

実際に修正された時のdiff
https://github.com/mruby/mruby/commit/ed0d9f0066eb541ae4ceddf47bdd85112feccddf

コンパイル時にレジスタ使い切るとクラッシュ

mrubyのVM上のレジスタは最大で512個なのだが、これを超えて使うようなソースコードを読み込ませるとクラッシュするというもの。
具体的にはローカル変数が512個あるコードなどが当たる。
(mrubyのVMではローカル変数一つにレジスタが一つ割り当てられている)

これは単にエラー処理の実装が間違っていて、解放済みメモリーを触ってしまっているだけ。
このエラー処理に分岐するのがレジスタ使い切りぐらいしか存在しないため、エンバグ後発見されなかっただけと思われる。

実際に修正された時のdiff
https://github.com/mruby/mruby/commit/56a93a3357ab9f50a4105980e0b4d39e7d400e3b

mrubyのバイトコードフォーマット解説

この投稿はmruby Advent Calendar 2013の3日目の記事です。
2日目はsuzukazeさんのmruby-redisでランキングを実装、4日目はdycoonさんのGUIアプリケーションなどが保持するmrubyのオブジェクトのGC対策です。

はじめに

mrubyのエンジンはソースコードを逐次解釈して実行しているわけではなく、専用のバイトコードに変換して保持し、それをVirtualMachineの上で解釈して実行しています。
であれば当然コンパイル済みバイナリファイルというのも存在するわけで、ならば当然どんな仕組みになっているか気になるわけです。

というわけで、大まかなファイルフォーマットについて調べたので今回説明しようと思います。

2013/11/25当時(git hash: f5bd87b9e6d0d8a84cf866b4847c1416e4f5c622 )の物です。
それ以降のmrubyを使用する場合は以下の解説の通りではない可能性があります。

全体の構造

ヘッダーとセッションの配列からなります。
ヘッダーは必ず一つ、セッションは複数個ですが必ず終端セクションで終わります。

ヘッダー

C風に書くとこんな感じ

ubig8_t signature[4];
ubig8_t version[4];
ubig16_t crc;
ubig32_t size;
ubig8_t compiler_name[4];
ubig8_t compiler_version[4];

※ubig%d_t: 符号なし %d bitビッグエンディアンの整数です、ただしubig8_t配列は文字列の場合もあります

signature: 0x52 0x49 0x54 0x45固定(ASCIIでRITE)
version: 0x30 0x30 0x30 0x32固定(ASCIIで0002)
crc:

uint16_t CalcCrc(const uint8_t * const ptr, const unsigned int size) {
  unsigned int a = 0;
  for (unsigned int i = 0; i < size; i++) {
    a |= ptr[i];
    for (unsigned int l = 0; l < 8; l++) {
      a = a << 1;
      if (a & 0x01000000) {
        a ^= 0x01102100;
      }
    }
  }
  return a >> 8;
}

※ptrはファイル全体のうちヘッダーのはじめからcrcまでを除いた部分、sizeはそのバイト数

こんな感じらしいのだが実際動かしてみるとずれるのでたぶんエンバグしている、が、めんどいので調べていない。
mrubyの/src/crc.cに実装があるので、知りたい人はそちらを見るとよい。

size: ファイル全体の長さ、ヘッダーも含む
compiler_name: コンパイルに使われたコンパイラーの識別子、特に使用されていない
compiler_version: コンパイルに使われたコンパイラーのバージョン、特に使用されていない

セクション

セクションは全部で3種類存在します。

  • 実行コード本体
  • 元となったソースコードの行番号情報
  • 終端セクション

詳しい説明は後で書くとして、共通のセクションヘッダーは以下の通りです。

ubig8_t signature[4];
ubig32_t size;

signature: セクションの識別子です、ここでセクションが何の情報を持つかが分岐します、0x49 0x52 0x45 0x50(ASCIIでIREP)か0x4C 0x49 0x4E 0x45(ASCIIでLINE)か0x45 0x4E 0x44 0x00(ASCIIでEND\0)のどれかになります。
size: セクションのサイズです、共通のセクションヘッダーのサイズも含みます。

linenoセクション

元となったソースコードのライン番号情報です、signatureは0x4C 0x49 0x4E 0x45(ASCIIでLINE)になります。
linenoの追加ヘッダー以下の通りです。

ubig16_t count;
ubig16_t start;

count: ライン情報の個数です。
start: よくわかりません。

ここにはlinenoの詳細を書くべきなのでしょうが、ぶっちゃけ興味なかったので調べておらずわからないのですっ飛ばします。

endセクション

終端セクションです、signatureは0x45 0x4E 0x44 0x00(ASCIIでEND\0)になります。
追加ヘッダーは特にありません。

このセクションが存在した時点でセクションの読み取りは終了となります。
一応ファイルサイズなどの情報もあるのでこの情報はなくても実行できますが、おそらくは利便性か速度上の都合なのでしょう。

irepセクション

実行コード本体です、signatureは0x49 0x52 0x45 0x50(ASCIIでIREP)になります。
irepの追加ヘッダー以下の通りです。

ubig8_t version[4];

version: irepの命令セットのバージョンです、現在は0x30 0x30 0x30 0x30(ASCIIで0000)固定です。

また、irepデータ本体を1つ以上持ちます。
いくつになるかは実際にirepデータを読んでみるまでわかりません。

irepデータ

関数1つやクラスの定義など1スコープの処理をまとめたものです。
定義は以下の通り

ubig32_t size;
ubig16_t localCount;
ubig16_t registerCount;
ubig16_t childCount;
ubig32_t code_count;
Code code[code_count];
ubig32_t pool_count;
Pool pool[pool_count];
ubig32_t symbol_count;
Symbol symbol[symbol_count];

size: irepデータのサイズです、sizeそのものも含みます。
localCount: ローカル変数の個数らしいですが何に使うのかわかりませんでした。
registerCount: このirepデータを実行するのに必要なレジスタの数です。
childCount: このirepに紐づく子のirepデータの数です。
code_count: irepバイトコードの個数です。
code: irepバイトコードです。
pool_count: リテラルプールの数です。
pool: リテラルプールです。
symbol_count: シンボルリストの数です。
symbol: シンボルリストです。

さらにchildCountの分だけ、irepデータが後に続きます。
それらのirepデータにも当然childCountがついているので、childCountが1の場合でも何十個とirepデータが続いている可能性があります。

リテラルプール

コード中で使用する文字列/整数/実数のリテラルを保持しています。
各リテラルはフォーマット上は文字列として保管され、パース時点でmrb_valueへと変換されます。
定義は以下の通り

ubig8_t type;
ubig16_t len;
ubig8_t body[len];

type: mrb_vtypeの値がそのまま入っており、0:文字列、1:整数、2実数となります。
len: bodyの長さです。
body: 文字列で表現されたリテラルで、終端記号(\0)は含まれていません。

シンボルリスト

コード中で使用するシンボル(メソッド名やクラス名など)を保持しています。
各シンボルはフォーマット上は文字列として保管され、パース時点でmrb_symへと変換されます。
定義は以下の通り

ubig16_t len;
ubig8_t body[len];
ubig8_t terminate;

len: bodyの長さです。
body: 文字列で表現されたシンボルです、リテラルと違いこちらは続くterminateが必ず0x00固定なのでC言語文字列として扱うことができます。
terminate: 必ず0x00固定です、bodyをC言語文字列として扱うためで、実際load.cを覗いてみるとchar *にキャストして使用されています。

irepバイトコード

VM上で実行できるバイトコードです。
すべての命令がオペコードと引数を合わせて4byteとなっています。
書くまでもないですが、定義は以下の通り

ubig32_t body;

body & 0x0000007F: オペコードです、2013/11/25時点では0~75まで定義されています。
body & 0xFFFFFF80: 引数ですが、オペコードにより引数の数とサイズが異なります。

各オペコードの説明まで始めるとそれだけで本一冊書けてしまうのと、ファイルフォーマットとは関係が薄いためここでは解説しません。

終わりに

いかがだったでしょうか。
実はオペコード関連も多少は読んでいて、ローカル変数はインデックス管理で名前が消滅しているとか面白い話もあったのですが、文中でも書いたように一冊本が書ける勢いなので省略しました。
mrubyを使う上ではあまり役に立たないかもしれませんが、個人的には面白かったです。

おまけとして、上記通りにパースして、ちょっとだけ命令の翻訳も行うようなアプリを書いてみたのでおいておきます。

parse_mruby2.zip

ではまた。

※2014/05/14付けでLVARセクションが追加されました、それらについての追加情報は以下を参照のこと。
rubyのバイトコードフォーマット解説その2