結局、インスタンスから値を取り出すにはキーバリュー方式とゲッター方式、どっちがいいの?
Railsアプリを作っていてそんな疑問をもったため、今回は**「処理速度」**というアプローチから、簡単な方法でそれぞれを測定してみました。
結論:ゲッター使った方が処理は速いかも。ちょっっっっっっとだけね。
■100000回、複数のインスタンス(2個)からnameとpriceを取り出した場合
Benchmark.bm 10 do |x|
x.report "result7_1" do
10000.times do
@items.each do |item|
item.name
item.price
end
end
end
end
Benchmark.bm 10 do |x|
x.report "result7_2" do
10000.times do
@items.each do |item|
item[:name]
item[:price]
end
end
end
end
user system total real
result7_1 5.110335 0.016141 5.126476 ( 5.124922)
result7_2 10.747493 0.038407 10.785900 ( 10.790415)
環境
- Ruby : ruby 2.6.5p114 (2019-10-01 revision 67812) [x86_64-darwin20]
- Rails : 6.0.0
- 追加ライブラリ => PryRails
測定方法
今回はRuby標準ライブラリである**「Benchmark」**を利用
###状況
- Railsアプリの作成中
- controllerファイルにて、モデルからインスタンス変数を作成してviewファイルに渡そうとしている。
def index
@items = Item.includes(:user)
end
# Itemには例としてnameとpriceというカラムがあると仮定する
# { name: 'apple', price: '200' } としておこう
一方でviewファイルにて以下のように取り出そうとしている
<% @items.each do |item| %>
<%= item[:name] %>
<%= item[:price] %>
<% end %>
別にこれで問題があるわけではない。
一方でこの記述はこうも書き換えられる。
<% @items.each do |item| %>
<%= item.name %>
<%= item.price %>
<% end %>
## 予備知識
この仕組みについて、私と同じような初学者の方向けに説明します。
ゲッターとActiveRecordの関係をご存知の方はスキップください。
■ゲッターについて
Rubyではクラスに設定したインスタンス変数の値を、インスタンスから読み取って表示する役割があるゲッターと呼ばれるメソッドが存在します。(というより、そういう書き方があります)
具体的には以下に定義するnameメソッド、priceメソッドを指します。
class Item
def initialize(name, price)
@name = name
@price = price
end
def name #これがゲッター
@name
end
def price #これもゲッター
@price
end
end
item = Item.new('apple', 200)
puts item.name #これでnameを呼び出せる
# => "apple"
puts item.price #これでpriceを呼び出せる
# => "200"
■ActiveRecordとゲッター
RailsにおけるモデルはActiveRecord(モデルがテーブル操作に関してできるメソッド群…詳しくは長くなるので割愛)という機能を継承しているのですが、モデルに対応するテーブル上に存在するカラム名をもとに、上述のゲッターとセッター(代入機能のメソッド…ここでは割愛)を自動生成してくれます。なんて便利。
つまり、RailsにおいてはActiveRecordのおかげで我々はitem.name
というゲッターを用いた記述でインスタンスの値を取り出すことができるわけですね。
以上、閑話休題。
検証
そこで考える。どちらかに統一した方が良いのではないかと。
今回は処理速度の速さに着目してみた。使用するのはRuby標準搭載のライブラリであるBenchmark。
Benchmarkの仕様については公式リファレンスへ
まず準備としてcontrollerファイルには以下のように記述。
require 'benchmark'
class ItemsController < ApplicationController
def index
@items = Item.includes(:user)
binding.pry
end
end
これでライブラリを読み込むことができたので、実際にアクションでインスタンス(インスタンス変数)が作成された段階でbinding.pry!
コンソール上でBenchmarkを実行してみた
■まずはItemモデルのインスタンス一つに対してnameを取り出してみよう
item = Item.find_by(id: 1) #Itemモデル内id=1のインスタンスを代入
Benchmark.bm 10 do |x|
x.report "result1" { item.name }
x.report "result2" { item[:name] }
end
すると結果は…
user system total real
result1_1 0.000072 0.000058 0.000130 ( 0.000125)
result1_2 0.000028 0.000023 0.000051 ( 0.000049)
あ、ふーん。
い、意外と差がない?
■複数のインスタンスから取り出してみよう
ではこれが複数の場合はどうなるのか。
以下はインスタンスが2つだった時の例。
Benchmark.bm 10 do |x|
x.report "result2_1" do
@items.each { |item| item.name }
end
x.report "result2_2" do
@items.each { |item| item.name }
end
end
結果はこうだ。
user system total real
result2_1 0.004093 0.002050 0.006143 ( 0.009102)
result2_2 0.000021 0.000002 0.000023 ( 0.000022)
なるほどちょっと差が出たように見える。(わかっていない)
ではもう少し数を増やしてみよう
■100個のインスタンスから取り出したら?
user system total real
result3_1 0.000162 0.000062 0.000224 ( 0.000214)
result3_2 0.000141 0.000031 0.000172 ( 0.000172)
あれ、順当に差がつくと思ってたけど…
■10000回試行してみよう
今度はインスタンスの数を2つにして、それぞれからnameを取り出す作業を10000回行ってもらった。
Benchmark.bm 10 do |x|
x.report "result4_1" do
10000.times do
@items.each { |item| item.name }
end
end
x.report "result4_2" do
10000.times do
@items.each { |item| item.name }
end
end
end
user system total real
result4_1 0.283555 0.000957 0.284512 ( 0.284478)
result4_2 0.523570 0.001647 0.525217 ( 0.524935)
あ、あれ?逆転している??
と、ここである可能性に気がついた。
**同じテスト内(Benchmark do ~ end)で複数回の試験をやると2回目以降のテストの方が結果が早くなるのではないか?**と
試しに10000回の試行でまったく同じ処理(item.name
)を行なった結果が以下の表だ。
Benchmark.bm 10 do |x|
x.report "result5_1" do
10000.times do
@items.each { |item| item.name }
end
end
x.report "result5_2" do
10000.times do
@items.each { |item| item.name }
end
end
x.report "result5_3" do
10000.times do
@items.each { |item| item.name }
end
end
end
user system total real
result5_1 0.286004 0.000920 0.286924 ( 0.287060) #1回目
result5_2 0.248555 0.000951 0.249506 ( 0.249520) #2回目
result5_3 0.249164 0.000926 0.250090 ( 0.250004) #3回目
やはり、userの部分で1つ目と2つ目の間で明らかな差異がある。
…これはフェアじゃないな。
ということで、仕切り直してそれぞれでテストを分けて計測をしてみた
計測の方法自体は同じだが、それぞれを別のテストとして実施してみた。
■単一インスタンスから取り出した場合
user system total real
result6_1 0.000030 0.000033 0.000063 ( 0.000048)
result6_2 0.000028 0.000041 0.000069 ( 0.000051)
この時点でtotalは逆転。
■10000回、単一インスタンスからnameを取り出した場合
user system total real
result6_1 0.009506 0.000068 0.009574 ( 0.009572)
result6_2 0.015251 0.000093 0.015344 ( 0.015370)
■10000回、複数のインスタンス(2個)からnameとpriceを取り出した場合
Benchmark.bm 10 do |x|
x.report "result7_1" do
10000.times do
@items.each do |item|
item.name
item.price
end
end
end
end
Benchmark.bm 10 do |x|
x.report "result7_2" do
10000.times do
@items.each do |item|
item[:name]
item[:price]
end
end
end
end
user system total real
result7_1 0.541852 0.001515 0.543367 ( 0.543375)
result7_2 1.101339 0.018870 1.120209 ( 1.135520)
■100000回、複数のインスタンス(2個)からnameとpriceを取り出した場合
user system total real
result7_1 5.110335 0.016141 5.126476 ( 5.124922)
result7_2 10.747493 0.038407 10.785900 ( 10.790415)
というわけで、ここまで試行すれば結果は瞭然。
ほぼ2倍の違いでゲッターを利用した方が処理が早いのである。
結果
インスタンスからバリューを取り出す、という処理において、速度という側面から見た場合はゲッターを利用した方が優位である。
ただ、今回の検証で大きな差異を確認できたのは試行回数が10000回を超えた段階だ。実際個人が作成するようなアプリケーションではここまでたくさんのインスタンスが保存されているかどうかはわからないし、一度に読み込むかと言うとそれは設計次第と言えよう。
もし、大きなデータを扱い、かつ、それを一度に読み込まなくてはならないという状況においては、この差を知っておくとユーザー目線で見た時によりストレスフリーなアプリケーションを作ることができるのかもしれない。
何より、せっかくRailsというフレームワークによって自由に使える選択肢が増えたのであるから、その恩恵を授かっても何ら悪いことではないように思う。しかし、今回はあくまでBenchmarkを使った「処理の速さ」という視点から捉えたに過ぎない。きっと私が思い付かないような側面が、まだまだあるのだろう。だから、この記事はあくまで一つの視点として留めといていただきたい次第である。
最後に
さて、今回はしごく簡単な検証をしてみたわけですが、Benchmarkの使い方について完全に理解できているわけではありませんし、私はまだプログラミングの学習を始めて4ヶ月の初学者です。
ひょっとすると、私のようにRailsの学習中に同じ悩みを抱えている同胞がいるやもしれぬと思い始めた試みではありますが、何より今、自身がスッキリしています。
今後も自身が学習を進める中で突き当たった壁や疑問について、深掘りしていくことができればと思っております。なお、今回は自身の持ちうる知識の範囲内での検証であるため、当然諸先輩方から見れば指摘すべき箇所があるかと存じます。ぜひコメント欄にてご教授をいただきたいと思いますので、よろしくお願いいたします!