0
0

More than 1 year has passed since last update.

【Rails】インスタンスからバリューを取り出す方法を処理速度から考える

Last updated at Posted at 2021-12-15

結局、インスタンスから値を取り出すにはキーバリュー方式とゲッター方式、どっちがいいの?

Railsアプリを作っていてそんな疑問をもったため、今回は「処理速度」というアプローチから、簡単な方法でそれぞれを測定してみました。

結論:ゲッター使った方が処理は速いかも。ちょっっっっっっとだけね。

100000回、複数のインスタンス(2個)からnameとpriceを取り出した場合

terminal
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
terminal
              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ファイルに渡そうとしている。
items_controller.rb
def index
  @items = Item.includes(:user)
end

# Itemには例としてnameとpriceというカラムがあると仮定する
# { name: 'apple', price: '200' } としておこう

一方でviewファイルにて以下のように取り出そうとしている

index.html.erb
<% @items.each do |item| %>
  <%= item[:name] %>
  <%= item[:price] %>
<% end %>

別にこれで問題があるわけではない。
一方でこの記述はこうも書き換えられる。

index.html.erb
<% @items.each do |item| %>
  <%= item.name %>
  <%= item.price %> 
<% end %>

 予備知識

この仕組みについて、私と同じような初学者の方向けに説明します。
ゲッターとActiveRecordの関係をご存知の方はスキップください。

ゲッターについて
Rubyではクラスに設定したインスタンス変数の値を、インスタンスから読み取って表示する役割があるゲッターと呼ばれるメソッドが存在します。(というより、そういう書き方があります)
具体的には以下に定義するnameメソッド、priceメソッドを指します。

sample.rb
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ファイルには以下のように記述。

controllers.rb
require 'benchmark'
class ItemsController < ApplicationController
  def index
    @items = Item.includes(:user)
    binding.pry
  end
end

これでライブラリを読み込むことができたので、実際にアクションでインスタンス(インスタンス変数)が作成された段階でbinding.pry!

コンソール上でBenchmarkを実行してみた

まずはItemモデルのインスタンス一つに対してnameを取り出してみよう

terminal
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

すると結果は…

terminal
              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つだった時の例。

terminal
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

結果はこうだ。

terminal
              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個のインスタンスから取り出したら?

terminal
              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回行ってもらった。

terminal
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
terminal
              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)を行なった結果が以下の表だ。

terminal

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つ目の間で明らかな差異がある。
…これはフェアじゃないな。


ということで、仕切り直してそれぞれでテストを分けて計測をしてみた

計測の方法自体は同じだが、それぞれを別のテストとして実施してみた。

単一インスタンスから取り出した場合

terminal
              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を取り出した場合

terminal
              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を取り出した場合

terminal
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
terminal
              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を取り出した場合

terminal
              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の学習中に同じ悩みを抱えている同胞がいるやもしれぬと思い始めた試みではありますが、何より今、自身がスッキリしています。

今後も自身が学習を進める中で突き当たった壁や疑問について、深掘りしていくことができればと思っております。なお、今回は自身の持ちうる知識の範囲内での検証であるため、当然諸先輩方から見れば指摘すべき箇所があるかと存じます。ぜひコメント欄にてご教授をいただきたいと思いますので、よろしくお願いいたします!

参考文献

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0