最近mrubyにコミットしているので自分の活動をまとめます。
mrubyを小さくした話
mrubyでは、文字列の扱いはシンプルにchar*
を構造体でラップしていました。
struct RString { MRB_OBJECT_HEADER; mrb_int len; union { mrb_int capa; struct mrb_shared_string *shared; } aux; char *ptr; };
そのため1つの文字列毎に、構造体分と文字列分の2回のmalloc/freeが発生していました。
ここでCRubyのRStringを見てみます。
#define RSTRING_EMBED_LEN_MAX ((int)((sizeof(VALUE)*3)/sizeof(char)-1)) struct RString { struct RBasic basic; union { struct { long len; char *ptr; union { ong capa; VALUE shared; } aux; } heap; char ary[RSTRING_EMBED_LEN_MAX + 1]; } as; };
str->as.heap
とstr->as.ary
の2パターンが有ることがわかります。
これは「文字列の長さ」+「ポインタ」+「メモリの長さ/Shared先ポインタ」を足せば結構なバイト数(3ポインタ分)あるから、この範囲の文字列なら長さとか使ってないフラグ領域に押し込んで直接char型のデータを持てばメモリ確保しなくて済む。よってmalloc/freeの数やメモリが減らせる。というテクニックです。(名前はEmbed化でいいのかな……)
以下この方法で作られた短い文字列を仮にEmbed-Stringと呼びます。
CRubyのコードを読んでいてこのことがわかっていたので「少メモリを謳うmrubyでやってないのはもったいない」と思って実装してみました。
mrubyの詰め込める量も現状3ポインタ分(最初はこんなことも知らずに実装しちゃい、後ほど別の方に修正していただきました。。。)。32bit環境なら3 * 4=12byte、64bit環境なら3 * 8=24byte分までの文字列なら詰め込めそうです。文字の長さはフラグの使っていない領域に覚えさせればOK。僕でもやれそうです。
C-API自体を変えてしまうため割りと修正が広範囲です。
そこで順序立てて実装していく戦略を取りました。(訳:勢いでやったら訳が分からなくなってやり直す羽目になった)
1. 取り敢えず現状の2回malloc/freeロジックのまま構造体を修正して各所のデータの引き方を修正していく
構造体さえ変えてしまえば後はコンパイラがおかしいところを教えてくれるので順次潰していきます。
静的言語さまさまですね。
2. ちょっとずつ新しい方法に置き換えていく
まずコードを読みやすくするためと作業の簡単化のためにマクロを導入します。
既存のマクロもEmbed-Stringだった場合でメモリの読み方を変えるように修正しておきます。
文字列をGCで回収する、要はfreeするところも1箇所しか無いのでここも修正してfreeしないようにします。
これで準備OK。
ここからは、新しい文字列を作るときに長さが短ければEmbed-Stringとして文字列を使う関数を別に用意して、影響の少なそうなところから文字列の作成関数を置き換え->修正後の呼び方でデータをひけるように修正(ほとんどはマクロで吸収できる)->テスト->だめなら修正のサイクルを回していきます。
文字列は他にも定数文字列「Nofree」とデータを共有する「Shared」と種類があるので、今どの種類でどの種類にすべきか、注意深く考えて修正していかなければなりません。
少しずつcommitしてセーブポイントを作っておくのもコツ。あとでrebaseすればいいんです。
外堀から徐々に埋めていけば問題を切り出しやすいので少しずつ進んでいってる感があるので良いですね。
本丸は文字列リテラルから呼ばれるところ、mrb_str_new
です。
ここさえ攻略できれば大方完成です。
3.テスト
特に気になるのは種類の変換をしているところです。文字列を追加して拡張したり、共有したり変更したりといったところは十分にカバレッジを気にしてテストを書きます。
valgrindでメモリーリークがないかチェックするのも重要です。
rake test valgrind --leak-check=full build/host/test/mrbtest
こまめに修正->テスト->valgrind->commitとやっていかないと訳が分からなくなって全部やり直し!なんてことにもなりかねません。セーブはこまめに、が基本ですよね。
4.緊張のPR
考えうる部分を全てやりきったらPRします。
ちゃんと何がやりたいのか、何でやりたいのかをissueに書いておきます。(この英文を書くのに1時間かかった……。) この作業を通して、これは別問題だなと思った箇所はPRを送っていたので初めてではないですが、githubでのスター数2000超えのリポジトリに大規模な修正PRを出すのでちょっと指が震えますが、えいやと送ってしまいましょう。
ここはOSSの世界。「mergeされないならまあそれはそれで勉強になったしいいやー。」ぐらいの気持ちでいることが大切です。
というか僕のようなどこの馬の骨ともわからない者がいきなり拙すぎる英語でAPIを変更する大きなPRを投げてきた、という状況なのでほぼ諦めていたのですが、見事mergeされました。
https://github.com/mruby/mruby/pull/1820
matzの懐の深さは宇宙レベルやで……!
これにより短い文字列を作った時のmalloc/free回数とその分のメモリ量を削減できました。
mrubyを大きくした話
僕がRubyに触れたきっかけは約一年前、JavaScriptでRubyのEnumeratorを実装してみたのがきっかけでした。
https://github.com/ksss/ruby-enumerator.js
これが1ヶ月くらいかけて書いて、4ダウンロード/月と大盛況。
そんなわけでEnumeratorには思い入れがあったわけです。
特にwith_indexなんかはお気に入りのメソッドで
> [1,2,3].map.with_index.to_a => [[1, 0], [2, 1], [3, 2]]
カッコイイですよね。
これをmrubyでやってみるとどうでしょう、
> [1,2,3].map.with_index.to_a mrblib/enum.rb:83: undefined method 'call' for nil (NoMethodError)
がびん。
そう、mrubyにはEnumeratorがなかったわけです。
「mrblib/hash.rbのeachにあるドキュメントは嘘かいな。」とも思ったのですがここは無いなら作るの精神です。mgem(mruby界のrubygems)としてmruby-enumeratorを作りました。
これライセンス的に大丈夫?と思いながらCRubyのEnumeratorのコードを見てmrubyとして移植するだけだったので作業自体はすぐ出来ました。
https://github.com/ksss/mruby-enumerator
リンク先を見れば解るのですが、これが本体に取り込まれることになりました。
最初、そんな気はさらさらなかった日陰根性の僕は「やっとまともなmgemが書けたなー」と思ってmgemにPRを投げます。
[https://twitter.com/ksss/status/443537653066002433:embed#mrbgems mruby-enumerator作りました。https://t.co/6qyjO19NUz]
https://github.com/mruby/mgem-list/pull/70
「ファッ!!?」
最初はギャグだと思ったのですが、「やれるだけやってみるか」と思い直して一旦Close.
ドキュメント等を整備してmruby本体にPR。
https://github.com/mruby/mruby/pull/1853
mergeされました。
「mrubyにはEnumeratorがない。」とは割と言われていたみたいですが、もうそんなことは言わせません。(バグがあったら教えて下さい……。)
最新のリポジトリでは
> [1,2,3].map.with_index.to_a => [[1, 0], [2, 1], [3, 2]]
もできるようになっています。
まとめ
mrubyはまだまだ人手不足だと思うので皆さんも参加してみてください。
例え馬の骨でもここはOSS。
僕でも参加できたのだから、誰でも参加できるはずです。
僕からは以上です。