Rails

RailsのSQLキャッシュと変数を使い回す場合のパフォーマンス比較、7000倍差

概要

SQLキャッシュを使うより、変数を使い回すほうが圧倒的に早い
例えば1ユーザの情報を表示するような場面ではパフォーマンスが気になるレベルではないと思われる。
表のようなデータを表示するような場合に同じデータをたくさん表示するような場合はちゃんと変数に入れたほうが、パフォーマンスが良い

はじめに

RailsのActiveRecordの仕組みに同じSQLを何度も読んでもキャッシュしてくれて、都度DBにアクセスしなくて良い便利な仕組みがある。

この機能はかなり便利で雑にプログラミングしてもパフォーマンスが落ちにくい。落ちにくいがハイパフォーマンスを求められるところや、多量のデータを表示、作成するような場合だと影響を与えるような気がする。

そこでSQLキャッシュを使った場合と、普通に変数に保存した値を使いまわした場合でベンチマークをとってみた。

SQLキャッシュが効いているかログで確認

キャッシュが効いていない場合のログはこのようにSQLを発行している

User Load (2.0ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = $1 LIMIT $2  [["id", 2], ["LIMIT", 1]]

キャッシュが効いる場合のログはこのように頭にCACHEがついている

CACHE User Load (0.0ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = $1 LIMIT $2  [["id", 1], ["LIMIT", 1]]

前提条件

Rails 5.1.4
Ruby2.5.0

測定

DBと連携しているUserモデルを呼び出し、ベンチマーク機能で10回、1000回測定している

SQLキャッシュを使う場合

@user = User.find(1)

benchmark("SQL cache") { 10.times { User.find(1) } }
SQL cache (7.9ms)
benchmark("SQL cache") { 1000.times { User.find(1) } }
SQL cache (705.2ms)

変数に格納した値を使い回す場合

@user = User.find(1)

# 参照を渡すだけの場合
benchmark("plane val") { 10.times { user = @user } }
plan val (0.0ms)
benchmark("plane val") { 1000.times { user = @user } }
plan val (0.1ms)

# 値を複製して渡した場合(別のオブジェクトにする)
benchmark("plane clone val") { 10.times { user = @user.clone } }
plane clone val (0.1ms)
benchmark("plane clone val") { 1000.times { user = @user.clone } }
plane clone val (1.6ms)

結論

結果から圧倒的に変数を使いまわしたほうが速いことがわかる
雑に計測したら

  • 参照で使い回す場合は7052倍早い
  • 複製する場合で440倍速い

ということがわかった。

ジュニアレベルのプログラマへのアドバイスとして「メモリやCPU、ファイル読み込み、データベース、ネットワークの気持ちになってプログラミングしろ」とよく言うんだけど、大体半笑いされるんだけどさこの結果からもパフォーマンスが求められるレベルのことをやりたかったら前述のセリフは大事だよねというのがわかるはず。はずだよね?

おまけ

キャッシュはおそらく設定ファイルのキャッシュの設定で動いているが、ファイルキャッシュとメモリキャッシュで設定を変えてみてもパフォーマンスの差は出なかった。ここらへん詳しい方コメント下さい

config.cache_store = :file_store
config.cache_store = :mem_cache_store

参考

ちなみにSQL Cacheを使わない場合と使った場合を測定したところ200倍速いそうだ
https://gist.github.com/ololobus/785f1005da29850d6a85

Railsは簡単にベンチマークがとれる
https://qiita.com/akishin/items/cf74559875cf475bbf66