RubyKaigi 2019 Keynote レポート

Jeremy Evansさん「たのしいRubyの先に,はやいRubyがある。Work, Correct, Fun! Fast」 〜RubyKaigi 2019 3日目 基調講演

この記事を読むのに必要な時間:およそ 3 分

メソッドの生成方法 / defとdefine_methodのどちらを利用するのか

基本的に,define_methodは通常のdefにくらべて50%遅く,defメソッドを利用すべきです。ただし,実行時にdefメソッドによってメソッドを生成する場合には,evalを併用するため,セキュリティに関する問題が発生することがあります。

例えば,モデルのカラムに対してgetterやsetterを設定するときに,class_evalとevalのなかでdefを実行すると,"employee name"のような空白を含むカラムがあった場合に動作しないため,define_methodを利用する必要があります。この場合も,一般的なものにはdefメソッドを利用し,カラムに空白を含むような例外的な場合にのみdefine_methodを利用することで,性能と安全性の両方を満たしています。

ループ処理の最適化

SQLAnywhereアダプターでループの内部でカラム名とカラムの型を繰り返し取得していますが,これらはループの中では変わるものではないため,いったん取得した後にローカル変数に代入して使い回しています。例えば100カラムある10,000行の結果を受け取る場合に,ループの外側で列名と列のほうをローカル変数に入れておけば,ループで200万回のメソッド呼び出しを削減できます。

速いアルゴリズムを利用する

数千ものルーティングがあった場合,Sinatraが利用しているようなO(n)のアプローチでは時間がかかります。Rodaではルーティングの数が増加しても性能が線形的に悪化しないようになっています。ツリーの一番上を探索する場合に,デフォルトでは線形であったのをmulti_routeプラグインを採用してO(n)からO(log(n))に変更しています。

さらに高速化を図るために,いくつルーティングがあっても同じ性能になるように,static_routingプラグインを持ち,O(1)を実現しています。これは最も高速であり,ルーティングの数が10から10,000に増加しても,性能の変化は15%ほどしかありません。しかしながら,これではRodaの他の機能を活用できないため,性能のメリットと複雑な機能のメリ ットの両方を持つhash_routesというプラグインが開発されました。これは"/foo/123/bar"のような形式に対応し,ネストの段階ごとにhashのキーにマッチするルートを探っていくものです。

可能な限りキャッシュする 全体を不変にして,局所的に変更可能にする

すべてのオブジェクトを不変にすることではなく,オブジェクトの状態を不変にした上で,オブジェクトが利用する変更可能なhashをキャッシュとして利用可能にすることで,性能と信頼性を両立させています。

Sequel::Datasetを例にすると,オブジェクトの状態はOPTSというfrozenなハッシュが保持しています。また,cacheというインスタンス変数も持っており,これはfrozenではありません。また,このcacheはmutexを利用したプライベートメソッドによってのみ変更され,スレッド間のキャッシュの整合性を保つようになっています。その後,オブジェクト自体をfreezeさせます。そうすることで,変更可能なのがcacheだけになるようにします。Sequel datasetsは性能向上のためにキャッシュを最大限に活用しています。

例としては,生成されたSQL文をキャッシュするのがもっとも効果がありました。すでにキャッシュされたSQL文があればそれを返し,なければ生成します。また,実行時に変化しない場合に限ってそれをキャッシュに保存します。

メタプログラミングとキャッシュの併用

メタプログラミングを活用し,メソッドチェーンの各段階の結果をキャッシュさせてmetaprogrammingで生成させることで,チェーンされるメソッドそれぞれの結果を自動的にキャッシュする機能を採用しました。

例えば,Album.released.by_name.firstというメソッドチェーンがあった場合にも,Album.released,Album.released.by_name,Album.released.by_name.firstとそれぞれの段階でキャッシュを保持します。もし,この処理を100回実行した場合にも,わずか3つのデータセットを保持するだけでよく,またSQLも1回だけ実行すれば済みます。キャッシュをしない場合の300のデータセット,100回のSQL実行に比べて,高速化を図っています。

正規表現よりStringを使う。StringよりIntegerを使う

さらに,/の文字列を比較する代わりに,それに対応するASCIIコードである47というIntegerをgetbyte関数を利用して比較することで,さらなる高速化が図られることも触れていました。

最適化は最後に

多くのパフォーマンスに関連するテクニックを紹介しましたが,パフォーマンスの最適化はいちばん最後に行うべきであることも述べていました。まず動くものを作り,修正して,楽しんだ後に最適化すべきとのことです。また,最適化にはトライアル&エラーが必須であるとし,benchmarkやbenchmark-ipsを利用してベンチマークを行うこと,ruby-prof stackprofなどを利用してプロファイルを取得することをアドバイスしていました。

まとめ

最近のRubyKaigiのキーノートでは,主にRuby処理系自体や,C言語に関するトピックが中心だったこともあり,Rubyで書かれたWebアプリケーションフレームワークという「高レベル」なソフトウェア開発者による発表に,やや意外な印象を持っていました。しかしながら,キーノートの最初から最後まで,密度の高いパフォーマンスに関する発表でした。正直,私には理解の及ばない点もいくつもありましたので,興味のある方は後日公開されるであろうキーノートの録画をご覧になることをおすすめします。

Jeremyが紹介した手法には,長期的にも守るべき原則もあった一方,キーワード引数が遅いなどの課題はRubyの処理系の改善によって性能が劣化しないようになってほしいものもあります。それらのテクニックがRubyKaigiで披露された結果,将来のRubyが「ふつうの」コードを書くだけで十分に速くなれば,それも一つのキーノートの効果といえるでしょう。なお,ここで記述されたActive Recordのコードは,このキーノートの後に更新されています。すでに発表のよい影響が出ているもといえるでしょう。

(写真提供=RubyKaigi 2019)

著者プロフィール

本多康夫(ほんだやすお)

Active Record Oracle enhanced adapterのメンテナー /
Ruby on Rails contributor /
Oracle ACE

freee株式会社所属。Asakusa.rb,OSSパッチ会,OSS Gate東京ミートアップ for Red Data Toolsに参加している。

Twitter:@yahonda
GitHub:@yahonda