Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails関連

Rails 7: ActiveRecord::Base.loggerがclass_attributeで7倍高速化(翻訳)

Rails 7: ActiveRecord::Base.loggerがclass_attributeで7倍高速化(翻訳)

私たちの見解では、このプルリクはRails 7におけるきわめてシンプルかつ大きなパフォーマンス改善です。最近のRubyで、クラス変数の読み取りにインラインキャッシュが導入されました(#177631。これにより、クラス変数の値解決で複雑な継承ツリーをたどるかわりにキャッシュから値を読み取れるようになりました。Rubyでクラス変数が読み込まれると、継承ツリーにある各クラスをチェックして、そのクラス変数がツリー内の他のクラスに設定されていないことを確認する必要があります。

もうお気づきかと思いますが、これはO(n)問題になります。ツリー内のノード数が増えるにつれて、読み取りのパフォーマンスは線形に低下します。

それでは、1個のモジュールを継承するクラス、30個のモジュールを継承するクラス、最後に100個のモジュールを継承するクラスを使ったデモを見てみましょう。

require "benchmark/ips"

MODULES = ["B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", "AA", "BB", "CC", "DD", "EE", "FF", "GG", "HH", "II", "JJ", "KK", "LL", "MM", "NN", "OO", "PP", "QQ", "RR", "SS", "TT", "UU", "VV", "WW", "XX", "YY", "ZZ", "AAA", "BBB", "CCC", "DDD", "EEE", "FFF", "GGG", "HHH", "III", "JJJ", "KKK", "LLL", "MMM", "NNN", "OOO", "PPP", "QQQ", "RRR", "SSS", "TTT", "UUU", "VVV", "WWW", "XXX", "YYY", "ZZZ", "AAAA", "BBBB", "CCCC", "DDDD", "EEEE", "FFFF", "GGGG", "HHHH", "IIII", "JJJJ", "KKKK", "LLLL", "MMMM", "NNNN", "OOOO", "PPPP", "QQQQ", "RRRR", "SSSS", "TTTT", "UUUU", "VVVV", "WWWW"]
class A
  @@foo = 1

  def self.foo
    @@foo
  end

  eval <<-EOM
    module #{MODULES.first}
    end

    include #{MODULES.first}
  EOM
end

class Athirty
  @@foo = 1

  def self.foo
    @@foo
  end

  MODULES.take(30).each do |module_name|
    eval <<-EOM
      module #{module_name}
      end

      include #{module_name}
    EOM
  end
end

class Ahundred
  @@foo = 1

  def self.foo
    @@foo
  end

  MODULES.each do |module_name|
    eval <<-EOM
      module #{module_name}
      end

      include #{module_name}
    EOM
  end
end

Benchmark.ips do |x|
  x.report "1 module" do
    A.foo
  end

  x.report "30 modules" do
    Athirty.foo
  end

  x.report "100 modules" do
    Ahundred.foo
  end

  x.compare!
end

キャッシュなしのRubyでは以下の結果になります。

Warming up --------------------------------------
            1 module     1.231M i/100ms
          30 modules   432.020k i/100ms
         100 modules   145.399k i/100ms
Calculating -------------------------------------
            1 module     12.210M (± 2.1%) i/s -     61.553M in   5.043400s
          30 modules      4.354M (± 2.7%) i/s -     22.033M in   5.063839s
         100 modules      1.434M (± 2.9%) i/s -      7.270M in   5.072531s

Comparison:
            1 module: 12209958.3 i/s
          30 modules:  4354217.8 i/s - 2.80x  (± 0.00) slower
         100 modules:  1434447.3 i/s - 8.51x  (± 0.00) slower

それではキャッシュありのRubyの結果を見てみましょう。

Warming up --------------------------------------
            1 module     1.641M i/100ms
          30 modules     1.655M i/100ms
         100 modules     1.620M i/100ms
Calculating -------------------------------------
            1 module     16.279M (± 3.8%) i/s -     82.038M in   5.046923s
          30 modules     15.891M (± 3.9%) i/s -     79.459M in   5.007958s
         100 modules     16.087M (± 3.6%) i/s -     81.005M in   5.041931s

Comparison:
            1 module: 16279458.0 i/s
         100 modules: 16087484.6 i/s - same-ish: difference falls within error
          30 modules: 15891406.2 i/s - same-ish: difference falls within error

Rubyのmasterブランチでは、モジュール100個をincludeするとモジュール1個のincludeの8.5倍遅くなります。しかしキャッシュを使えば、モジュール1個のincludeとモジュール100個のincludeのパフォーマンスは変わらなくなります。

それでは、Railsコアチームがこのパフォーマンス向上をどのようにRailsに取り入れたかを見てみましょう。

変更前

ActiveRecord::Base.loggerは継承ツリーに63個2のモジュールを持つcvar(クラス変数)です。以下を実行すればこのことを確かめられます。

ActiveRecord::Base.ancestors.size

# => 62

ActiveRecordコアのコードを見てみると、ロガーは以下のように定義されています。

mattr_accessor :logger, instance_writer: false

ここがクラス変数としてではなくmattr_accessorとして定義されているので、このままでは最新のRubyで導入されたパフォーマンス改善が効いてくれません。

変更後

Railsへのプルリク#42237によって、loggerの定義方法が以下のように変更されました。

class_attribute :logger, instance_writer: false

それではパフォーマンスを比較して改善を確かめてみましょう。

Calculating -------------------------------------
              logger      1.700M (± 0.9%) i/s -      8.667M in   5.097595s
             clogger     11.556M (± 0.9%) i/s -     58.806M in   5.089282s

Comparison:
             clogger: 11555754.2 i/s
              logger:  1700280.4 i/s - 6.80x  (± 0.00) slower

7倍近い高速化は大きな改善です!現実のRailsアプリケーションが強化された見事な例です。

注意

この変更にはいくつかの注意点があります。

ActiveRecord::Base.loggerclass_attributeになったため、@@logger で直接アクセスできなくなります。また、サブクラスに logger = を設定しても親クラスのロガーを変更できません。

logger はほとんどの場合単なるメソッドとして使われているので、きわめてささいな不都合に過ぎませんが、注意するに越したことはありません!

この改修が皆さんのRailsアプリのコードベースで効くかどうかを今のうちに調べておきましょう。

関連記事


  1. 訳注: Rubyのこの機能は2021年5月に#4340でmasterブランチにマージされているので、利用できるのはRuby 3.1以降となります。 
  2. 訳注: Rails 7 alpha2では70個でした。参考まで。 

CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。