mruby v1.3

mruby v1.3 がリリースされましたね。

趣味mrubyウォッチャーとしてv1.2からv1.3で何が変わったのかを、個人的にまとめてみたいと思います。

注目すべきは、やはりmatzのcommit数。

もちろんmerge commitも含みますが、約半数のcommitがmatzのcommitになっています。

なぜmatzがここまでmrubyに力を入れるのか聞いてみたいところですね。

それではmruby v1.2 からv1.3への変更で何が変わったのか、ザックリ見ていこうと思います。

リリースノート

http://mruby.org/releases/2017/07/04/mruby-1.3.0-released.html

1年以上あった割には、表向きにはそこまで変化はない感じ? わりと最新のCRubyの文法やメソッドも入っていたりしますね。

Contributions

https://github.com/mruby/mruby/graphs/contributors?from=2015-11-17&to=2017-07-04

v1.2が出た日からv1.3が出た日にしているので若干正確ではないのですが、やはりmatzのcommit数はダントツですね。

Many bug fixes

やはり注目すべきはShopifyからの大量のbug報告と、これら全てを丁寧に対応されたmatzの修正の応酬でしょう。

https://github.com/mruby/mruby/issues?q=is%3Aissue+author%3Aclayton-shopify

SEGVの報告。free後のメモリや、確保した領域外のメモリなど、触れてはいけないメモリを触れてしまっている部分の修正。GCのバグ修正。意図しない無限ループの修正。諸々含めて約200のissueが上げられ、閉じられています。

shopifyとmrubyの関係はbovi氏のblogが詳しい。

The 500.000$ release - mruby.sh

こちらからの目線での印象としては、正直「そんな重箱の隅まで……。」というケースもあったのですが、 ユーザーにコードを書いてもらって実行するようなシステムだと、「ユーザーにシステムを止められるようなコード書かれると困る」というのは分かる話。

おかげでmruby本体が原因でSEGVが起きるようなケースは、重箱の隅を含めてもそう滅多なことでは起こらないようになったんじゃないでしょうか。

mrb_yieldでbreakできない問題

折角なのでどういうbug fixがあったのかひとつ抜き出してみましょう。

この問題が修正されたのは1.3が出る直前のことでした。

v1.2では、実は以下のようなコードはうまく動きません。 breakしているのに、ループが終わらないのです。っていうかSEGVします。

$ mruby-1.2.0
"aaa\nbbb\nccc\n".lines do |line|
  p line
  break
end
#=> "aaa\n"
#=> "bbb\n"
#=> "ccc\n"
[2]    72458 segmentation fault  mruby-1.2.0

これは、Cで実装されたメソッドでmrb_yield系が使われていた場合にbreakを利用すると発生していました。

breakreturnとほぼ同じ扱いにしているので、rubyで書かれたブロックならbreakで以降のコードは飛ばせるのですが、 C側のコードは引き続き実行してしまうのでループを止められないという問題があったようです。

Rubyコード上はbreakしているのに、C側のコードは止められないので不整合が起こり、SEGVしているものと思われます。

この問題はbreakを例外扱いすることでlongjumpし、Cのレベルでもメソッドのコードを飛ばせるように修正されています。

Allow `break` from a block called by `mrb_yield`; close #3359 · mruby/mruby@d4d99dd · GitHub

パフォーマンス

「パフォーマンスは早くなったの?」

ということで、mrubyに添付されているbenchmarkスクリプトで計測してみました。

buildの設定はfull-coreにしただけでその他は初期値です。(OSX 10.12.5 clang v8.1.0)

$ time mruby-1.2.0 benchmark/bm_ao_render.rb > /dev/null
mruby-1.2.0 benchmark/bm_ao_render.rb > /dev/null  20.22s user 1.44s system 98% cpu 21.942 total

$ time mruby-1.3.0 benchmark/bm_ao_render.rb > /dev/null
mruby-1.3.0 benchmark/bm_ao_render.rb > /dev/null  19.74s user 0.11s system 98% cpu 20.134 total

$ time mruby-1.2.0 benchmark/bm_fib.rb > /dev/null
mruby-1.2.0 benchmark/bm_fib.rb > /dev/null  12.32s user 0.03s system 99% cpu 12.387 total

$ time mruby-1.3.0 benchmark/bm_fib.rb > /dev/null
mruby-1.3.0 benchmark/bm_fib.rb > /dev/null  13.33s user 0.05s system 98% cpu 13.529 total

$ time mruby-1.2.0 benchmark/bm_app_lc_fizzbuzz.rb > /dev/null
mruby-1.2.0 benchmark/bm_app_lc_fizzbuzz.rb > /dev/null  37.46s user 0.45s system 98% cpu 38.559 total

$ time mruby-1.3.0 benchmark/bm_app_lc_fizzbuzz.rb > /dev/null
mruby-1.3.0 benchmark/bm_app_lc_fizzbuzz.rb > /dev/null  41.14s user 0.37s system 99% cpu 41.850 total

$ time mruby-1.2.0 benchmark/bm_so_lists.rb > /dev/null
mruby-1.2.0 benchmark/bm_so_lists.rb > /dev/null  5.90s user 0.17s system 97% cpu 6.197 total

$ time mruby-1.3.0 benchmark/bm_so_lists.rb > /dev/null
mruby-1.3.0 benchmark/bm_so_lists.rb > /dev/null  48.57s user 0.53s system 99% cpu 49.492 total

うーん、そこまで劇的な変化はなさそうですね。

やはり、mruby v1.3の目玉は大量のbug fixということでしょうか。

bm_so_lists.rbはCRubyだと1sとかなのでこれは……見つけちゃったかもしれないですね……。

一応報告しておきましょう。

Performance regression for benchmark/bm_so_lists.rb · Issue #3737 · mruby/mruby · GitHub

[追記]なおしました。

Should only check frozen fix #3737 by ksss · Pull Request #3739 · mruby/mruby · GitHub

v1.4……?

keyword arguments

https://github.com/mruby/mruby/pull/3629

実はkeyword argumentsに対応するPRは既に来ています。

が、v1.3には取り込まれませんでした。

リリースノートにもRemaining Bugsとしてこっそり含まれているので、ゆくゆく対応されるのではないでしょうか。

※ここは個人のブログです

個人のブログなので、ここからは自分がやったことをまとめます。

Kernel#caller書いたのオレオレ

Cレベルではほぼ同じ機能があったので、これをRubyの世界でも使えるようにしただけ。

引数の扱いが意外とめんどくさかったりしました。

これでデバッグが楽になる場面が増えたはず……!

Proc#initialize消したのオレオレ

これまでは、Proc#initializeでProcオブジェクトの初期化を行っていたのですが、Proc.remove_method(:initialize)と凶悪なことをすると、Procオブジェクトに必要な値が準備されず、いろいろな部分でSEGVが発生するようになります。

これではCレベルのメソッドでProcオブジェクトを扱う際に、常にProc.remove_method(:initialize)されたことも考慮しないといけなくなってしまいます。

ところでCRubyを見てみると、なんとProc#initializeは定義されていないではありませんか。 つまりこういう問題は、CRubyではProc.remove_method(:initialize)にはProc#initializeを定義しないことによって対策していると判断。

mrubyでもProc#initializeを削除して、Proc.newメソッドを独自に定義するようにしました。*1

Proc.newで初期化するようにすれば、たとえProc.newが消されたとしてもProcオブジェクトが作れないのだけなので問題ありません。

Contributionsの二番目にいるのオレオレ

https://github.com/mruby/mruby/graphs/contributors?from=2015-11-17&to=2017-07-04

mruby-specを使って、mrubyとCRubyの挙動の不整合をひたすら直すということをしてました。 またその際にみつかったバグなども直したり、難しすぎて直せないものはissueで報告などの活動をしていました。

序盤の活動はブログに書いていましたね。

結構Rubyの細かい挙動を知れたりして勉強にもなりました。

shopifyのやつもちょこっとだけ手伝った

Make string embad from shared by ksss · Pull Request #3657 · mruby/mruby · GitHub

例えばこちらのエントリーでは、shopify issueの https://github.com/mruby/mruby/issues/3651 を解決しています。

前知識としてStringオブジェクトは、shared, no-free, embed, normalなどの状態が存在し、今どの状態かによって適切に処理を分岐しないと、最悪SEGVするケースがあります(大抵は破壊的な操作)。

sharedなStringを変更してsharedの関係をやめるときに、これまではnormalなStringとしていたのですが、 issueにあるような一部のコードでは文字列の長さでembedかどうか判断していました。

じゃあこれを適切に場合分けして……とも思ったのですが、normalなのに長さはembed可能な文字列が生成できてしまうせいで、問題が起こっていると判断。sharedなStringを変更するときに、長さが短い場合はembedなStringにしてしまえば、問題も治るしメモリ消費を若干抑えられるしで一石二鳥です。

bugを一つ埋め込んでしまった

MRB_DISABLE_STDIO: String#upto native's mrb_str_upto requires snprintf · Issue #3714 · mruby/mruby · GitHub

で報告されているとおり、mruby-string-ext gemをMRB_DISABLE_STDIOでbuildできなくしてしまいました。。。

MRB_DISABLE_STDIOはその名の通り標準入出力を行う関数を一切禁止するだいぶリソースが厳しい環境向けのオプション。

そんな厳しい環境では標準添付ライブラリなんて使わないよね……。う……これで困る人、許してくれ。。。

mruby v1.3

みんな使ってね!

*1:ちなみに実装は、渡されたブロックのコピーを作っているだけ。