こんにちは、hachi8833です。引き続きNate Berkopec氏の記事をお送りいたします。rack_freezeとsnip_snipはRails開発で役に立ちそうです。
- 第1回: BootsnapやPumaなど -- 開発中のRailsをBootsnapで倍速起動するなど
- 第2回: rack-freezeやsnip_snipなど(本記事)
- 第3回: 「あなたのアプリサーバーの設定は間違っている」など
概要
原著者より許諾を得て翻訳・公開いたします。
- 元記事: Railsconf 2017: The Performance Update
- 著者: Nate Berkopec (@nateberkopec): Railsのパフォーマンスコンサルタントです。
- 主著: The Complete Guide to Rails Performance
楽しい画像はすべて元記事からの引用です。
RailsConf 2017のパフォーマンス関連の話題(2)rack-freezeなど(翻訳)
4. rack-freeze
パフォーマンス関連ではよく「自分のRubyアプリがスレッドセーフかどうかをどうやって確認すればよいのでしょうか?」という疑問を耳にします。私が普段用意している答えは、テストをマルチスレッドで実行するというものです。
しかしこのアドバイスには2つの問題があります。1つはRSpecはマルチスレッドで実行できないという点です。したがってこの方法はminiTestに限られます。2つ目は、この方法は単体テストやアプリケーション単位でのスレッドバグを見つける場合にしか通用しないことです。依存関係についてはほとんどカバーできません。
Rackミドルウェアはスレッドバグの発生源の1つです。この問題は基本的に以下のような感じになります。
class NonThreadSafeMiddleware
def initialize(app)
@app = app
@state = 0
end
def call(env)
@state += 1
return @app.call(env)
end
end
この問題は、すべてのRackミドルウェアであらゆるものをfreeze
するという興味深い方法であぶり出すことができます。
上のコード例で言うと、@state +=
行ではマルチスレッドアプリに誤った方法で暗黙に値を追加する代わりに、そこでコケてRuntimeErrorを返すようになります。rack-freeze gemは上のコードに対してまさにこのように動作します。作者である@schneemsのためにもrack-freezeを広めましょう。
5. snip_snip
何の話題だったか思い出せませんが、会場の廊下でKevin Deiszと立ち話したときにsnip_snipというgemがあることを知りました。開発中などにbullet
を試してみた人はたくさんいると思います(bulletはアプリのN+1問題の検出に役立つgemです)。
snip_snip
はbullet
と少し似ていますが、こちらはSELECT
したのに使われていないデータベースカラム検出用のgemです。
class MyModel < ActiveRecord::Base
# 属性は :foo, :bar, :baz, :qux
end
class SomeController < ApplicationController
def my_action
@my_model_instance = MyModel.first
end
end
上に続いて以下を行うとします。
# my_action.html.erbの他の部分
@my_model_instance.bar
@my_model_instance.foo
するとsnip_snip
が「せっかくSELECT
した:baz
属性と:qux
属性が使われてないゾ!」と知らせてくれます。それではコントローラのアクションを次のように書き直してみましょう。
class SomeController < ApplicationController
def my_action
@my_model_instance = MyModel.select(:bar, :foo).first
end
end
全属性のSELECT
(デフォルトの動作)をやめて属性を絞り込むと、大量のActiveRecordオブジェクト(通常100個超え)を一括作成するときや、多数の属性を抱えているオブジェクト(User
など)を取得するときのスピードがよい感じに向上します。
6. Rubyのインライン展開
廊下でNoah Gibbsと立ち話したときのことです。Noahによると、Rubyのコンパイル時にコンパイラのinline threshold値を増やしたところ、少し速くなったそうです。
inline thresholdは基本的に、コードのセクションをコピペし、個別の関数を呼び出す代わりに1つの関数にインライン展開する作業をどの程度まで行ってよいかをコンパイラに指定します。コンパイル時にインライン展開するとプログラムで別の場所にジャンプするよりも速くなるのが普通ですが、もしプログラム全体を単純にインライン展開してしまうとRubyのバイナリサイズが1GBぐらいに膨れ上がってしまうかもしれません。
Noahによれば、inline threshold値を少し増やすとRubyのバイナリサイズが最大で3MBほど増加する代わりにoptcarrot benchmarkで5%から10%ほど高速化できたとのことです。これは多くの開発者にとってかなり魅力的なトレードオフになります。
以下は私が試した結果です。Mac環境のデフォルトのコンパイラであるClangを使う場合、CFLAGS
環境変数でコンパイラにいくつかのオプションを渡せます。
CFLAGS="-O3 -inline-threshold=5000"
Example with ruby-install
ruby-install ruby 2.4.0 -- --enable-jemalloc CFLAGS="-O3 -inline-threshold=5000"
GCCの場合は次のようにします。
CFLAGS="-O3 -finline-limit=5000"
今のところ、これをproduction環境で使う気にはなりません。というのも、私のローカル環境ではたまにsegmentation faultになるからです。もちろん、development環境で遊んでみる価値は十分あると思います。
続き: RailsConf 2017のパフォーマンス関連の話題(3)「あなたのアプリサーバーの設定は間違っている」など(翻訳)
関連記事
- RailsConf 2017のパフォーマンス関連の話題(1)BootsnapやPumaなど(翻訳)
- Railsで検索を高速化するならこれで決まり!Sunspotで始めるSolr入門
- [Rails 4.0] 巨大なテーブルやserializeを使うときのActiveRecordオーバーヘッドを測定してみた