RailsConf 2017のパフォーマンス関連の話題(2)rack-freezeやsnip_snipなど(翻訳)

こんにちは、hachi8833です。引き続きNate Berkopec氏の記事をお送りいたします。rack_freezeとsnip_snipはRails開発で役に立ちそうです。

  • 第1回: BootsnapやPumaなど — 開発中のRailsをBootsnapで倍速起動するなど
  • 第2回: rack-freezeやsnip_snipなど(本記事
  • 第3回: 「あなたのアプリサーバーの設定は間違っている」など

概要

原著者より許諾を得て翻訳・公開いたします。

楽しい画像はすべて元記事からの引用です。

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_snipbulletと少し似ていますが、こちらは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)「あなたのアプリサーバーの設定は間違っている」など(翻訳)

関連記事

Ruby on RailsによるWEBシステム開発、Android/iPhoneアプリ開発、電子書籍配信のことならお任せください この記事を書いた人と働こう! Ruby on Rails の開発なら実績豊富なBPS

この記事の著者

hachi8833

Twitter: @hachi8833、GitHub: @hachi8833

コボラー、ITコンサル、ローカライズ業界、Rails開発を経てTechRachoの編集・記事作成を担当。
これまでにRuby on Rails チュートリアル第2版の半分ほど、Railsガイドの初期翻訳ではほぼすべてを翻訳。その後も折に触れてそれぞれ一部を翻訳。
かと思うと、正規表現の粋を尽くした日本語エラーチェックサービス enno.jpを運営。
実は最近Go言語が好き。
仕事に関係ないすっとこブログ「あけてくれ」は2000年頃から多少の中断をはさんで継続、現在はnote.muに移転。

hachi8833の書いた記事

BPSアドベントカレンダー

週刊Railsウォッチ

インフラ

BigBinary記事より

ActiveSupport探訪シリーズ