More than 1 year has passed since last update.

mrubyが対象(のひとつ)にしているデバイス系環境では、プログラムを格納するROMには余裕があっても、実行時メモリであるRAMには余裕がないという話をよく聞きます。ROMはフラッシュでメガ単位の容量があっても、RAMは数十KBとかいうケースも珍しくありません。

そこで、12/24から25にかけての24時間で、どれだけメモリ削減できるか一人ハッカソンを開催しました。ぼっち上等。

基本戦略

今回のメモリ削減ハックの基本となるのは、Cの文字列定数領域はfreeの対象にならないので、mallocした領域にコピーする必要はない、という戦略です。今回は3件の改善を行いますが、いずれもこの戦略を用いています。

static symbol

まずは、symbolテーブルのmalloc量を削減しましょう。symbolの登録には

mrb_intern(mrb_state *mrb, const char *str, size_t len)

を用いますが、これは内部的に渡された文字列領域をコピーしてsymbolテーブルに登録しますが、元になる文字列領域がCの文字列定数であれば、コピーはムダです。

幸い、最近C文字列定数からのsymbol生成を支援する mrb_intern_lit() マクロが導入されましたので、これを用いてsymbol生成された時にはコピーを行わないように改造しましょう(bc7a6e)。

mrubyのリポジトリを手元にもってきて、

git show bc7a6e

すると、この変更を見ることができます。新たな制約として、symbolの最大長が16bit整数になっていますが、パーサーですでに1024文字制限が入っていますから問題になることはないでしょう。一応、長さチェックも入れています。

測定

せっかくのハックが効果があったのかベンチマークを取る必要があります。

今回はmrubyのテストスイートであるmrbtestを使ってどのくらいメモリが割り当てられたのかを測定することにします。こういう測定にはvalgrindがピッタリです。

valgrind --leak-check=full build/host/test/mrbtest

を実行することで、メモリリークチェックに加えて、メモリアロケーションの回数とメモリアロケーション量を測定することができます。

実行結果の末尾を見ると

==28787== 
==28787== HEAP SUMMARY:
==28787==     in use at exit: 0 bytes in 0 blocks
==28787==   total heap usage: 72,465 allocs, 72,465 frees, 4,595,938 bytes allocated
==28787== 
==28787== All heap blocks were freed -- no leaks are possible
==28787== 
==28787== For counts of detected and suppressed errors, rerun with: -v
==28787== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)

とあります。この72,465がメモリアロケーションの回数であり、4,595,938がメモリアロケーション量です。
これは変更前の数値です。

さて、今回のハックを行ったものに対して同様の測定をするとアロケーション回数が72,324、アロケーション量が4,594,802になります。ということは、アロケーション回数で141回、アロケーション量で1136バイトの節約になります。たった1Kか。

load.c symbol

思ったより効果がなかったので、更に改善できるところがないか考えてみましょう。

mrbc -cでコンパイルされたプログラムはCの配列に変換されますから、これもコピーする必要はありません。
そこで、load.cをいじってC配列から生成するsymbolのコピーもmallocを省略することにしましょう(bd76b2)。

これに合わせてマクロmrb_intern_litの実体関数の名前をmrb_intern_staticに変更しています。

これも測定すると、アロケーション回数が69,487、アロケーション量が4,566,384になります。回数で2978回、アロケーション量で29554Bの節約です。29KBかあ。

load.c string

考えてみたら、mrbcが生成したC配列の中にはsymbolだけでなく文字列も含まれています。こっちの方が効果が大きそうです(48ad07e)。

これも測定してみましょう。回数が65,382、アロケーション量で4,518,140です。当初から比較すると7078回と77798Bの改善です。

(追記)

内部表現から文字列を作る関数mrb_str_pool()の中でもアロケーションを避ける改善を行ったところ、さらに削減されて、回数が61365回、アロケーション量が4471885Bになりました。124KBの改善です

まとめと今後

というわけで、ちょいちょいとハックしてmrubyの消費メモリを124KBほど削減しました。
総消費量に比較するとわずか2.7%ほどですが、塵も積もれば山となるかもしれません。

同じ方針でメモリ量を削減できそうなのは命令列の部分ですが、エンディアンの問題もあり、mrbcファイルのフォーマットを変更せずには実現できそうになかったので、今回は見送りました。そのうちmrbcにエンディアン情報を付け加えて実装しようと思います。

この調子でmrubyは、皆様からのアイディアやpull requestをお待ちしています。