パフォーマンスの改善

続いてパフォーマンスです。パフォーマンスについて言うと、どんな言語も速すぎるということはあまりありません。必ず「遅い」って文句言う人がいます。Rubyは伝統的に文句付けられっぱなしの言語なので、それはそれで「どうなの?」って感じですけども。

もっとたくさんのトラフィックを捌くために、いろいろ改善が必要だと思っています。

昨年中国に行って「Ruby Conf China」で中国の人たちと話す機会があったんですが、昨年のことなので「MJIT」がちょうど開発中で、パフォーマンス改善が非常にホットなトピックスでした。

中国の人たちといろいろ話をしたんですが、「JITコンパイラは明日いるものではない」と。「私たちにとって一番必要なのは、メモリーでのボトルネックの改善である」ということなんですね。中国でも、例えばアリババなどわりと大きなサイトでRubyが使われているようです。

その中で彼らが一番最初にぶち当たっているボトルネックがメモリである、ということを言われましたので、GCの改善、メモリ管理の改善が非常に重要だと思いました。

Rubyについても、例えば2.1で世代別GC、2.2でIncremental GC、2.6ではTransient Heapを、2.7については、Object Compactionの改善を行ってきています。

これらの改善によって、Rubyでよく使われていた「Out Of Bands GC」はもう必要なくなったと言えます。

「Object Compaction」については、Aaron Pattersonさんが、2日目に話してくれるのであまり説明しませんが、Fragmentationの解消によって、トータルのメモリ消費を減らしたりキャッシュミスを減らしたりできるような、「可能であれば動かす」タイプのCompactionとなります。

いくつかのGCは、ガベージコレクションのタイミングで何も指定しないで勝手にコンパクトするんですけども、現在の「Object Compaction」は、ここでコンパクションしますということを明示的に宣言するタイプだと聞いています。

CPU・I/Oのボトルネックを解決する

メモリは、唯一のボトルネックではありません。その次にくるのがCPUで、その次にくるのがI/Oです。これらのボトルネックを改善するのも、われわれにとって重要なテーマです。

昨年はJITを使って、「Just-in-Time Compiler」を使って改善するということで、「MJIT」について、導入しました。RubyのバイトコードをCに変換して、そのCをGCCやClangを使ってコンパイルして、コンパイルした結果をダイナミックロードするというプロセスです。

移植性が高いのは非常にいい点です。GCCのサポートをしているOSやCPUは非常にたくさんあるので、これらの範囲内ではJITが使えるのはメリットですし、GCCは30年以上の歴史ある伝統的なソフトウェアなので、その点での信頼性は非常に高い。さらに、そのGCCのOptimizedは、信頼性が非常に高く最適化されているんですね。

欠点もあって、「マジかよ」って思ったぐらい重たいんです。毎回コンパイルのプロセスを立ち上げたり、正気かという感じなんですけども。その点もあって、特にメモリについて非常に不利な点もあります。もちろん最初からわかっているので、だいぶいろいろ手を加えてはいます。

ただ、このMJITですが、CPUがボトルネックになっているタスクに関しては、例えば、われわれのCPUボトルネックのベンチマークで「Optcarrot」ですね。ファミコンエミュレータですけども。

Ruby2.6は、Ruby2.0に比べて2.8倍実行速度が速いということで、「けっこういいじゃん」という感じではあるんですが、残念ながらRailsアプリに関していうと遅くなるという……。

(会場笑)

完全に余談ですが、最近PHP8が次のメジャー版でリリースでJITを入れるという話があって、「おぉ、すごいじゃん」って思ったんですけど。ベンチマークですごい速いとか、MJIT付けたRubyなんかよりずっとずっと速いとか聞いて「おぉ、すごい」と思ったんですけど。Webアプリケーションでは重くなって「やっぱりかぁ」という感じでした。

(会場笑)

理由はいくつかあって、RailsのようなWebアプリケーションフレームワークには非常にたくさんのメソッドがあるので、非常にたくさんのメソッドをコンパイルしないといけないんですね。それから、メモリ消費量も大きくなってしまうので、少なくともこの2つは大きなボトルネックになっています。

もしかしたら「Threasholds」を改善することによって、あまり呼ばれないメソッドはそもそもJITを掛けないとか、そういう選択肢も取れるんじゃないかと思います。それから「そもそもGCCを起動するから重い」のであって、MJITのベースを作ってくれたVladimir Makarovという人は、「MIR」というライトウェイトJITを作りました。それは昨年のRuby会議で発表してくれましたけど、あれをリファインして将来のRubyに入れるかもしれなくて、そうするとバーチャルマシーンとライトウェイトJITのMIRと、それからインナーループみたいな時間を掛けても最適化したいものにはMJITを掛けてという構図ができるかもしれないなと思ってます。

マルチコアに対応する

他にパフォーマンスを改善する方法は、やっぱりマルチコアですね。今までいろんなキーノートで、静的型の話やJITの話をたくさんしてきました。昨年までの間にこれらについて非常に多く進んだんですが、このマルチコアを使ってマシンにいっぱいあるコアを活用することについては、あまり発表していませんでした。

「Guild」という何かがあって、それを使うと「なんかうまくいきそう」ぐらいなことしか言っていなくて、今回のキーノートのタイトルの「The Year of Concurrency」というのは、今年は重点的GuildのConcurrencyのデザインをするつもりで付けたタイトルです。

もともと、ワールド・ワイド・ウェブというのは、マルチコアに向いたアーキテクチャではあります。

例えば、Unicornはマルチプロセスを使ってマルチコアを活用したり、Pumaはマルチスレッドを使ってコアを活用したり、最近でたFalconはファイバーを使ってボトルネックを解消するタイプですね。Falconについては、サミュエルが話してくれると思いますが、このようにコアを活用することがキーになってくると、コンカレンシーモデルをどうするかというのは、プログラミング言語の未来に非常に強く影響するのではないかと思っています。

ボトルネックの改善における課題

今のRubyはThreadとFiberを持っていますが、それぞれボトルネックの改善という観点では、やや課題を抱えています。

Fiberについては、明示的にコンテキスト切り替えの先を指定しなければいけないので、ボトルネックの改善には役に立ちづらいんですよね。一方Threadのほうは、Global Interpreter Lockというものがあって、バーチャルマシンの中では1つのスレッドだけが同時に動くようになっています。

この2点があるので、現在のコンカレンシーモデルであるThreadやFiberを使って、CPUボトルネックやI/Oボトルネックを改善するのは難しいんです。

そこで、もうちょっと良いコンカレンシーモデルを提供したいと思ってます。マルチコアを使えて、もっと使いやすくて、より安全なもの。鍵になるのは「Shared-Nothing」、状態を共有しないことだと思います。共有された状態というのは、コンカレントプログラムにおいてだいたい諸悪の根源なので、これをなんとかしたいと。

最初からコンカレントに設計された言語であるErlangやElixirは、基本的にデータ構造はイミュータブルになっていて書き変わらないので、状態について心配しないで良いのですが……。

Rubyはねぇ……。「これからRubyのオブジェクトを全部イミュータブルにします」って言われたら、みんな「ギャッ」って言うんで、それは出来ないんですね。

「それじゃあどうしようか」ということで考えたのが、「Guild」ですね。Guildについてご存知の方もいらっしゃると思います。Guildというのは、オブジェクトステートの分類になります。このGuildに所属しているオブジェクトは、他のGuildに所属しているオブジェクトに参照することができません。Guild間の通信はチャンネルを使って行います。ただ、「Deeply Frozen Objects」というアイデアを使って、効率を高めたいと思っています。

Guildは分離されたオブジェクト空間なので、他のGuildのオブジェクトに触ることはできないのですが、チャンネルを使って通信するときに、イミュータブルなオブジェクトしか受け渡しをすることができません。

受け渡しできるのは、数値か、Frozenな文字列か、シンボルか、Deeply Frozen Objectsです。それ以外のものを送ろうと思ったら、コピーをして送ることになります。

RubyにはFrozen Objectという概念があって、このオブジェクトは書き換えることができないと。例えば配列とかをフリーズすると、配列の要素を書き換えることができないというものが昔からあります。

ただ、配列の要素を書き換えることができなくても、要素としてさされているオブジェクトが書き変わったら、全体も書き変わって、状態も変わってしまうわけですね。

そこで、再帰的に参照しているオブジェクトが全部Frozenであるものについては、Recursively Frozen Objectとしてフラグを付けましょうと。そのフラグがついたオブジェクトはGuildを超えて受け渡しができますよ、ということを考えています。

そうするとImmutable Object Reference Graphができるので、それは自由に渡してもいいということです。

こうやって使うと、そのGuildはActorsみたいな使い方ができますし、JavaScriptのWebWorkersみたいな使いかたができるのではないかと考えています。WebWorkersの場合ですと、JavaScriptのプログラムは「このファイルの中身を実行してね」とか、「この文字列を実行してね」といったかたちで渡しますが、Rubyの場合はIsolated Blocksを使おうかと相談しています。まぁ、笹田(耕一)君のアイディアなんですけど。

Isolated Blocksというのは、Blocksと同じようなものですが、外のローカル変数やグローバル変数にアクセスできないタイプのブロックですね。

これを実装すると、マルチコアを使えて、CPU Intensive Tasksを改善できるようなものができそうです。現在のスレッドと同じぐらいの重さで実装することが、少なくとも当初の時点でできそうです。さらに、未来においてはN×M Concurrencyみたいなものを使って、よりライトウェイトにできるんじゃないかと思います。

めでたし、めでたし。おしまい。

違いますね!(笑)

I/Oボトルネックを改善している先行事例

Guildは今までのコンカレンシーモデルと比べて随分異なっているので、少なくとも慣れるまではちょっと使いづらい気がするのと、それから、I/Oボトルネック解消するには「向かない」というのは言い過ぎなんですが、使いづらいところがあって、そこでI/Oボトルネックを改善している先輩をちょっと見てみます。

Node.jsはそんな感じですけども、Node.jsってマルチスレッドじゃないんですよね、基本的に。シングルスレッドなんですけど、「たくさん捌けるよ」「パフォーマンス良いよ」みたいなことを言われるわけです。彼らはマルチコアを使ってなくても、そう言われるわけですよね。

その背景には、使っているV8のバーチャルマシンがめっちゃ速いというのと、Non-blockingI/Oは基本であるということがあります。I/Oボトルネックを解決するためには、Non-blockingI/Oが重要であるということで、Eric Wongなどが提案したアイディアを採用しようと思います。

彼は最初「AutoFiber」という名前にしたんですけど、そうすると名前が紛糾して、名前のために話が進まないという非常に悲しい状態になっているんですけど、基本的にI/O操作に対してコンテキストスイッチするタイプのスレッドです。

今までのRubyのスレッドと違って、時間で切り替わるということがありません。勝手に切り替わらないので、スレッドよりもはるかに簡単に使うことができます。I/Oブロッキングを避けることができると。これによってI/O Intensive Tasksを避けることができると思っています。

これが定着した暁には、現在ある今のRubyのスレッドというのは、段階的に使わなくしていくことができるんじゃないかなぁっと思ってます。

名前をどうするか

とくにスレッドによるコンカレンシーは非常に難しくて、スレッドを追加したことを後悔してるんです。スレッドを入れたときに私が使っていたコンピュータは1個しかCPUがありませんでしたし、マルチコアの時代が来るってぜんぜん予想してなかったんですね。「未来予想が下手くそかよ」って言われたら、その通りなんですけど。

あんまり深く考えないで、グリーンスレッドを……1.4とかでいったのかな? なんかすごい昔なんですけど。

いざ使ってみると、正しく使うのは難しいですし、効果的に使うのも難しいですし、さらにバグが出るときも難しいですし。ちょっとつらい感じになってしまったので、Rubyのコンカレンシーをもうちょっとマシにしたいと常々思っていました。そのための道具として、新たな機能として、Guild……。私個人としては、JavaScriptの一部で、Isolatesという名前を使っているので、そっちがいいんじゃないかなぁと思っていたら、笹田君に反対されました(笑)。これから名前に対してファイトしないといけないんですが。

さらにAutoFiberというのも、「それ、ファイバーじゃないよね」という指摘があって、まったくもってその通りなので、なんか名前つけなきゃいけないんですが、これが本当に分からないんですが……。でもどういう働きをするものなのかは分かってきたので、これを導入すると今までよりは使いやすいし、今までよりデバッグしやすいし、今までより性能を上げるのが簡単になるんじゃないかと思ってます。

GoやElixirは、最初からそういうコンカレンシーの機能を持っていて、例えばGoはgoroutineの1個しかない、スレッドやファイバーを使い分ける必要がないですし、Elixirはprocessというものがあって、それもやはりファイバーを使い分ける必要ないわけです。

ですが、われわれは残念ながら最初からコンカレント言語としてデザインしていなかったので、「不明を恥じます」という感じですけど。

さらに「今まで動いてるRubyのプログラムは動かなくなるよ」という状況にするわけにいかないので、使い分けるような2種類のものを入れるのが妥当じゃないかなぁと思います。

ですが、このGuildとAutoFiberは、今までのRubyのものに比べて随分よいじゃないかと思っています。

それで、われわれに必要なのは、名前……。

(会場笑)

どうしようね? 本当にね(笑)。

昨日、開発者ミーティングをしたんですけど、「いっそ今あるスレッドをAutoFiberみたいに動かしたらどう?」みたいな提案もありました。「それ互換性どうなの?」という感じなんですが、もうちょっと悩ませてください。

なんですが、私だけでなくて、みなさんの意見も大歓迎するので、もめるのは必至なんですけど、たくさんアイディアが出ると、その中に1個ぐらい光るのがあるかもしれないので、がんばって考えようと思います。