てくすた

ピクスタ株式会社のエンジニア・デザイナーがつづるよもやまテクニカルブログです

RubyKaigi 2018 3日目まとめ

こんにちは!ピクスタ開発部です。

f:id:Yasaichi:20180602180002j:plain

RubyKaigiの3日間、とても早かったですね! 最終日の3日目も、開発部のメンバーが選りすぐりのセッションを紹介します。

Ruby code from the stratosphere - SIAF, Sonic Pi, Petal

Raspberry Piを気球で打ち上げ、成層圏*1から受信したデータを使用してSonic Piで音楽を生成する取り組みについての発表でした。

Sonic Pi

Sonic Piは、Rubyで作られている音楽生成用のプログラムです。発表者の方は、このSonic Piをさらに簡潔に書けるようにPetalを開発しました。

require "~/petal/petal.rb"
cps 1
d1 'bd'

このように書くだけで、「1秒間に一度バスドラムを鳴らす」といった音楽が生成されます。 出力できる音は219種類用意されています。 これらを組み合わせることによって、デモのような音楽を作成することができます。

成層圏からの音

当初、地上と気球内のRaspberry Piのやりとりは

  1. 地上のPCでコードを生成
  2. 無線で気球内のRaspberry Piにコードを送信
  3. Raspberry Piで気象データを基に作った音を地上に送信

といった流れで処理を行っていましたが、地上で音を受信する際、気球の高度が上がるほどノイズが酷くなってしまう問題がありました。
この問題を解決するために、

  1. 気球内のRaspberry Piで気象データを基にSonic Piのコードを生成
  2. 無線で地上のPCにコードを送信
  3. 地上のPCで音を再生

という改良をしました。

成層圏で音を生成するまでの過程と、実際に緯度、経度、高度からなる音を発表中に聞けたのですが、打ち上げ~回収までの過程を知った後に聴くと、幻想的な気分になりました。 また、成層圏で気球から撮影された写真なども公開されています。

daily.siaf.jp

The Method JIT Compiler for Ruby 2.6

speakerdeck.com

"Ruby 3x3"の達成において大きな役割を担っているJITコンパイラを開発中の国分崇志さんによる、同コンパイラの現状と将来に関するセッションでした。

国分さんが開発しているJITコンパイラは、昨年のRubyKaigiにおけるVladimir MakarovさんのKeynoteで発表のあったMJITがベースになっています。こちらは既にRuby 2.6.0のpreview版に取り込まれていますが、Race Conditionによるバグが解消していないので、まだ本番環境には適用しないでほしいとのことでした。

セッションでは、まずJITコンパイラの実装と各OSへの対応状況、およびパフォーマンスの改善結果が紹介されました。現在の実装において、あるメソッドをJITコンパイルする流れをまとめると次のようになります:

  1. メソッドの内容に対応するYARV命令列からCのコードを動的に生成する
  2. 得られたコードからgccを使って共有オブジェクト(.so)を生成する
  3. 得られた共有オブジェクトを動的にロードし、これを呼び出す

ポイントは2で、動的に生成されたCのコードが(人類の叡智の結晶である)gccによってよしなに最適化されるので速くなる、というのが筆者 id:Yasaichi のざっくりとした理解です。

仕組みをある程度理解したら、次にJITを有効にしたRubyがどれくらい速くなるのかが気になるところです。セッションでは、最初にNESエミュレータのOptcarrotを使ったベンチマークの結果が示されました。現在のtrunkでJITを有効化すると、Ruby 2.0と比較して2.03倍高速化している、とのことでした。

「すごい、本当にRuby 3x3が達成できそう!」という希望が見え始めたところで、話はRuby on Rails (Discource)でのベンチマークの結果へ。実は、次のissueにもあるように、現状ではJITを有効にすると逆に遅くなってしまうという状況になっています。

bugs.ruby-lang.org

「なぜNESエミュレータは高速化できるのにRailsだと遅くなるのか、、?🤔」という問題提起がなされたところで、セッションは"JIT on Rails"のパートへ。国分さんは問題の原因について5つの仮説を立て、ひとつずつ検証・修正を行っていきます。こちらの詳細については、スライドを参照していただければと思います。

続く"Dive Into Native Code"のパートでは、先述した「gccによってよしなに最適化されるので速くなる」の「よしなに」の部分に対して詳細な解説がなされました。gccによってRubyで書かれた四則演算やwhileループがどのように最適化されるのか、ネイティブコードレベルで理解したい方はぜひご一読を。

個人的に印象的だったのが、"Method Inlining"と題された最後のパートです。メソッドをインライン化を推し進めれば、gccによる最適化の余地が大きくなって、高速化できる可能性が高くなります。インライン化の対象としてはRubyのメソッドとブロック、Cのメソッドが考えられますが、これらがCのコードから呼び出される場合にインライン化を行うのは難しく、メンテナンス性の低い実装になってしまうので対応は避けたい、とのことでした。
そこで、国分さんは「Cで書かれたメソッドをRubyで書き直して、Cより速くなれば問題ないのでは?」と発想を転換し、Cで書かれたInteger#timesをRubyで書き直してみたところ、見事高速化することに成功します。これは、Integer#timesメソッドとそのブロック引数の呼び出しが、次のようにどちらもインライン化しやすい形になったからと考えられます:

  • Integer#timesメソッドの呼び出し
    • 書き直し前: RubyからCのメソッド呼び出し
    • 書き直し後: RubyからRubyのメソッド呼び出し
  • ブロック引数の呼び出し
    • 書き直し前: CからRubyのブロック呼び出し
    • 書き直し後: RubyからRubyのブロック呼び出し

最後に、JITを前提とすれば、高速化のためにCではなくRubyを使った方が良い状況になったことから、"C language is dead"と書かれたスライドでセッションを締めくくりました。字面のインパクトに国分さんの話すスピードの速さが相まって、とても印象に残ったセッションでした。

How happy they became with H2O/mruby, and the future of HTTP

www.slideshare.net

H2Oの作者である奥さん(@kazuho)とフルタイムでH2Oにコントリビュートしている長田さん(@i110)による、H2Oを使ったバックエンドサーバーの改善と、H2Oの新機能ついての発表でした。

旧アーキテクチャ

インテリア写真SNSであるRoomClipでは、アップロードされた画像をリサイズ・マスクしているそうです。 旧アーキテクチャでは、バックエンドのサーバーとしてNginx+smalllightを使用していました。ただ、システムではURLに画像リサイズのサイズを指定できるのですが、意図しない値を設定できたりしていました。原因をまとめると以下のようになったそうです。

  • Nginxの設定ファイルはプログラミング言語ではないので、複雑なことをするのは難しい
  • Nginxでのデバッグやテストについて、環境が整っていない

新アーキテクチャ

新アーキテクチャでは、Ningx+smalllightだったところをH2Oに変更し、画像リサイズ処理はUnicorn+RMagickという構成で外出しを行ったそうです。 H2Oでは設定ファイルをmrubyで記述でき、mruby-mtestを使用すれば、ユニットテストも書くことが可能です。またデバッグについては、mrubyではみなさんお馴染みのp(printf)デバッグが可能です。

通常、Nginxの場合は実際のリクエストを投げながら設定ファイルが正しいかを確認する必要があると思いますが、H2Oではユニットテストを書けるので、実際のリクエストを投げなくてもテストコードで正しい動作かを確認することができそうですね。

H2Oの新機能

直近の新機能としては、H2O::Channel, H2O::TCPSocket, Server Timing, 103 Early Hintsがあるそうです。 H2O::ChannelはこちらのissuePRだと思うのですが、非同期で複数のリクエストを投げて、早く帰ってきたものをまとめて結果を返すということができたりするみたいです。

H2Oは使っていないのですが、弊社のサービスであるfotowaでも画像をアップロード・加工する場面があり、以下で紹介しています。

texta.pixta.jp

Design pattern for embedding mruby into middleware

speakerdeck.com

middlewareにmrubyを組み込む経験から得た知見についてのセッションでした。

なぜ、どのように組み込むか

mrubyをmiddlewareに組み込む理由は、以下が挙げられます。

  • 振る舞いに関わる部分を一部rubyで書きたかった
  • あまり自由に書けるとそれはそれで問題を引き起こすので、機能が限定されているmrubyはちょうどよかった。
  • mrubyで処理を書けると、動的に処理を制御できるようになる。

mrubyを組み込際には以下を意識していたとのことです。

  • メモリ管理をする、パフォーマンスが出せる、開発者が使いやすい
  • マルチスレッド、マルチプロセスに対応する

デザインパターン

6年ほどtry&errorを繰り返しながらmrubyのmiddlewareへの組み込みを行っていて、デザインパターンが確立したようです。 セッションでは以下の3つが紹介されていました。

init/exit

master/workerそれぞれでmrb_stateを使う際のやり方の話。 以下の順で異なるやり方が紹介されていました。

  1. masterでmrb_state作成、workerをfork後に新たにmrb_state作成してそこでinit処理を行う。init処理終了後に解放する。
  2. workerではmasterで作ったmrb_stateの上でinit処理を行い、init処理終了後に解放する。
    → 1から改善され、init時のスクリプト同士でobjectを共有できるようになった
  3. workerではinit時に解放せず、requestを受けたとき用にとっておく。exit時にまとめて解放する。
    → 2からさらに改善され、requestの際ににobjectの受け渡しができるようになった!(もちろんメモリ使用量は増えるのと、requestごとのメモリ管理が大変)

multi processes model / multi threads model

mrb_state(==mruby)の作成にはコストがかかるので、どのようなタイミングで作成しどのように使いどのタイミングで解放すべきか、など考慮する必要があります。 セッションでは、以下の順で改善して見せていました。

  1. リクエストが来るたびにmrb_stateを作りその都度解放する。
    → シンプルだが毎回作成するのでパフォーマンスが悪すぎる
  2. initでmrb_stateを作っておいて、requestが来たときにコンパイルして実行する。生成したbytecodeはその都度解放する。
    → リクエストごとにscriptを変更できるメリットはあるが、メモリ管理がかなり複雑
  3. コンパイルまで含めてinitで終わらせておく。requestが来たらbytecodeを実行してその都度必要最低限のcontextのみ解放する。基本的に大部分をexit時に解放する。
    → ハイパフォーマンス!(だがリクエストごとにscriptを変更できないデメリットはある。)

non-blockingなmiddlewareでmrubyを使う

mruby実行はblockingな処理をしてしまいます。 それ故何も考えずにmrubyを使えば使うほどrequestごとに待つ時間が増えて処理が遅くなってしまいます。 そこでblockするような処理はnon-blockな処理に変えて可能な限り平行に処理できるようにしたい、というお話です。

mrubyで最低限のblockingな処理を終わらせたら、さっさとmiddlewareのイベントループに返す。 そしてイベントループのコールバック関数によってmrubyの処理を再開させる!それによりその部分はnon-blockに処理を行うことができる! ということでした。

感想

mrubyをmiddlewareに組み込んで使った経験は皆無でしたが、わかりやすい図とともに丁寧な解説がされていたのでとても理解しやすく有意義な時間を過ごせました。 普段私はRubyをウェブアプリケーション作成や電卓としてしか使っていませんので、このようなセッションは新鮮でした。

おわりに

ピクスタの開発部では3日間を通して、RubyKaigiに参加することができなかった方でもイベントの内容が楽しめるように、実況ツイートをしてきました。 微力ではありますが、スポンサーになったことや、実況ツイートで、Rubyの取り組みに寄与できたのではないかと思います。

* * * * *

弊社では、Rubyへの貢献に興味があるかたを募集しています!

recruit.pixta.co.jp

*1:地球をとりまく対流圏の上層で、気温がほぼ一定した大気層