0
0

More than 3 years have passed since last update.

Ruby on Railsのパフォーマンスと可読性のイディオム集

Last updated at Posted at 2019-12-25

前提

Schemafile
# -*- mode: ruby -*-
# vi: set ft=ruby :
# 単語
require 'people.schema'
require 'person_addresses.schema'

people.schema
# -*- mode: ruby -*-
# vi: set ft=ruby :
create_table "people", id: :integer, force: :cascade, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4" do |t|
  t.string :japanese_name, null: false
  t.string :english_name, null: true

  t.timestamps
end
person_addresses.schema
# -*- mode: ruby -*-
# vi: set ft=ruby :
create_table "person_addresses", id: :integer, force: :cascade, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4" do |t|
  t.integer :person_id, null: false
  t.index [:person_id], name: 'index_person_addresses_on_person_id'

  t.integer :address_type, null: false

  t.timestamps
end
Terminal
bundle exec ridgepole -c config/database.yml -E development --apply -f db/schemas/Schemafile
app/models/person.rb
# == Schema Information
#
# Table name: people
#
#  id            :integer          not null, primary key
#  japanese_name :string(255)      not null
#  english_name  :string(255)
#  created_at    :datetime         not null
#  updated_at    :datetime         not null
#

class Person < ApplicationRecord

  has_many :person_addresses, dependent: :destroy
  accepts_nested_attributes_for :person_addresses, reject_if: :all_blank, allow_destroy: true
  validates :japanese_name, presence: true
  validates :english_name, format: { with: /\A[\p{ascii}]+\z/ }, allow_blank: true
end
app/models/person_address.rb
# == Schema Information
#
# Table name: person_addresses
#
#  id            :integer          not null, primary key
#  person_id     :integer          not null
#  address_type  :integer          not null
#  created_at :datetime         not null
#  updated_at :datetime         not null
#

class PersonAddress < ApplicationRecord

  belongs_to :person

  enum address_type: {
    one: :one,
    two: :two,
    three: :three,
    four: :four,
    five: :five,
  }, _prefix: true

  validates :person_id, presence: true, on: :update
  validates :address_type, presence: true
end

Active Record

pluck & any

pry(main)> `ruby -v`
=> "ruby 2.5.0p0 (2017-12-25 revision 61468) [x86_64-linux]\n"
pry(main)> Rails.version
=> "5.2.0"

CONVERT_MAP = {
  one: :one,
  two: :two,
  three: :three,
  four: :four,
  five: :five,
}.freeze

1000.times.each do |idx|
  person = Person.new(japanese_name: "#{idx}")
  person_addresses = 2.times.map{|_idx| PersonAddress.new(address_type: CONVERT_MAP.symbolize_keys.keys.sample)}
  person.assign_attributes(person_addresses: person_addresses)
  person.save!
end

Benchmark.bm 5 do |row| 
  row.report "pluck and any" do
    Person.last.person_addresses.pluck(:address_type).any? { |address_type| CONVERT_MAP[address_type.to_sym].present? }
  end
   row.report "where" do
    Person.last.person_addresses.where(address_type: CONVERT_MAP.keys)
  end
end 

=> [#<Benchmark::Tms:0x0000556c84fcb2f0
  @cstime=0.0,
  @cutime=0.0,
  @label="pluck and any",
  @real=0.009011553949676454,
  @stime=0.0,
  @total=0.006902000000000186,
  @utime=0.006902000000000186>,
 #<Benchmark::Tms:0x0000556c84fc7060
  @cstime=0.0,
  @cutime=0.0,
  @label="where",
  @real=0.004103021929040551,
  @stime=0.0,
  @total=0.0033479999999999066,
  @utime=0.0033479999999999066>]
=> [#<Benchmark::Tms:0x0000556c84aa8908
  @cstime=0.0,
  @cutime=0.0,
  @label="pluck and any",
  @real=0.008074268931522965,
  @stime=0.0,
  @total=0.006250000000000533,
  @utime=0.006250000000000533>,
 #<Benchmark::Tms:0x0000556c84b03448
  @cstime=0.0,
  @cutime=0.0,
  @label="where",
  @real=0.00198873202316463,
  @stime=0.0,
  @total=0.0015290000000005577,
  @utime=0.0015290000000005577>]
=> [#<Benchmark::Tms:0x0000556c83d5f670
  @cstime=0.0,
  @cutime=0.0,
  @label="pluck and any",
  @real=0.006150746019557118,
  @stime=0.0012540000000003104,
  @total=0.004831000000000252,
  @utime=0.0035769999999999413>,
 #<Benchmark::Tms:0x0000556c83d41f08
  @cstime=0.0,
  @cutime=0.0,
  @label="where",
  @real=0.0022412820253521204,
  @stime=0.0009770000000006718,
  @total=0.0018830000000011893,
  @utime=0.0009060000000005175>]
=> [#<Benchmark::Tms:0x0000556c83031bd8
  @cstime=0.0,
  @cutime=0.0,
  @label="pluck and any",
  @real=0.008198636001907289,
  @stime=0.0004149999999993881,
  @total=0.005551999999998891,
  @utime=0.005136999999999503>,
 #<Benchmark::Tms:0x0000556c83029730
  @cstime=0.0,
  @cutime=0.0,
  @label="where",
  @real=0.0025708150351420045,
  @stime=0.0005850000000000577,
  @total=0.002069999999998906,
  @utime=0.0014849999999988484>]
=> [#<Benchmark::Tms:0x0000556c85ba55f8
  @cstime=0.0,
  @cutime=0.0,
  @label="pluck and any",
  @real=0.011462070979177952,
  @stime=0.0,
  @total=0.007191999999999865,
  @utime=0.007191999999999865>,
 #<Benchmark::Tms:0x0000556c85b9b968
  @cstime=0.0,
  @cutime=0.0,
  @label="where",
  @real=0.0026785259833559394,
  @stime=0.00029100000000070736,
  @total=0.002032000000001588,
  @utime=0.0017410000000008807>]
  • どうも pluck のち any? などのeachは where よりも遥かに遅い様です。

ビルトインオブジェクト

Array

pluck

ASCII文字をランダム生成

  • ASCII文字自体の生成をするには、メタ
res = Benchmark.bm 10 do |row|
  row.report "ascii" do
    [*"\0".."~"].sample(200).join
  end  
end
res.each{|res| p [sprintf("%.5f", res.utime.round(5, half: :up)), sprintf("%.5f", res.stime.round(5, half: :up)), sprintf("%.5f", res.total.round(5, half: :up)), sprintf("%.5f", res.real.round(5, half: :up))].join(" | ") }
レポート名称  user system total real
ascii(1) 0.0 0.00018 0.00018 0.00011
ascii(2) 0.00026 0.0 0.00026 0.00034
ascii(3) 0.00005 0.00002 0.00007 0.00007
ascii(4) 0.00005 0.00002 0.00007 0.00007
ascii(5) 0.00007 0.00003 0.00011 0.00017

Arrayからあるパターンのキーに一致するものだけフィルタしてそのキーを取りたい場合

レポート名称 意味
forEach 繰り返しで索引した場合
keep_if keep_ifメソッドを使用した場合
select selectメソッドを使用した場合
images = []; 1000000.times {|row| images << "img#{row + 1}_url" }
result_count = {:forEach=>0, :keep_if=>0, :select=>0}
Benchmark.bm 10 do |row|
  row.report "forEach " do
    fin = 50000
    1.upto(fin) do |row|
      url_key = "img#{row}_url"
      next if images[row - 1] != url_key
      Struct.new(:url, :caption, :title).new(url_key.to_sym, url_key.to_s.gsub(/url/, "caption"), url_key.to_s.gsub(/url/, "title"))
      result_count[:forEach] += 1
    end
  end
  row.report "keep_if" do
    images.keep_if {|row| row.to_s.match?(/img([1-9]|[1-9][0-9]{1,3}|[1-4][0-9]{4}|50000)_url/) }.each do |url_key|  
      Struct.new(:url, :caption, :title).new(url_key.to_sym, url_key.to_s.gsub(/url/, "caption"), url_key.to_s.gsub(/url/, "title"))
      result_count[:keep_if] += 1
    end 
  end
  row.report "select" do
    images.select {|row| row.to_s.match?(/img([1-9]|[1-9][0-9]{1,3}|[1-4][0-9]{4}|50000)_url/) }.each do |url_key|  
      Struct.new(:url, :caption, :title).new(url_key.to_sym, url_key.to_s.gsub(/url/, "caption"), url_key.to_s.gsub(/url/, "title"))
      result_count[:select] += 1
    end 
  end
end

result_count
=> {:forEach=>50000, :keep_if=>50000, :select=>50000}

計測結果

1回目

レポート名称  user system total real
forEach 1.312843 0.026700 1.339543 ( 1.349017)
keep_if 1.768649 0.026811 1.795460 ( 1.808968)
select 1.481335 0.030547 1.511882 ( 1.523911)

2回目

レポート名称  user system total real
forEach 1.312844 0.034082 1.346926 ( 1.365920)
keep_if 1.766020 0.022876 1.788896 ( 1.797898)
select 1.439497 0.021102 1.460599 ( 1.467751)

3回目

レポート名称  user system total real
forEach 1.317690 0.018919 1.336609 ( 1.350585)
keep_if 1.763197 0.024135 1.787332 ( 1.804232)
select 1.443859 0.022458 1.466317 ( 1.472827)

4回目

レポート名称  user system total real
forEach 1.294830 0.015705 1.310535 ( 1.318776)
keep_if 1.958510 0.051190 2.009700 ( 2.040533)
select 1.321429 0.013722 1.335151 ( 1.340082)

5回目

レポート名称  user system total real
forEach 1.422966 0.018969 1.441935 ( 1.449615)
keep_if 1.733768 0.020578 1.754346 ( 1.769189)
select 1.451152 0.015573 1.466725 ( 1.472972)
  • パフォーマンスとしては大差ないが、可読性では後者となるkeep_ifが僅かに遅く、forEachとselectが誤差値考慮すると大差とは言い難いような印象である(可読性は後者にはなるが)

Hashからあるパターンのキーに一致するものだけフィルタしてそのキーを取りたい場合

レポート名称 意味
forEach 繰り返しで索引した場合
keep_if keep_ifメソッドを使用した場合
select selectメソッドを使用した場合
images = []; 1000000.times {|row| images << "img#{row + 1}_url" }; image_map = {}; images.map {|row| image_map[row.to_s.to_sym] = row }
result_count = {:forEach=>0, :keep_if=>0, :select=>0}

Benchmark.bm 10 do |row|
  row.report "forEach " do
    fin = 50000
    1.upto(fin) do |row|
      url_key = "img#{row}_url".to_sym
      next if image_map[url_key].blank?
      Struct.new(:url, :caption, :title).new(url_key, url_key.to_s.gsub(/url/, "caption"), url_key.to_s.gsub(/url/, "title"))
      result_count[:forEach] += 1
    end
  end
  row.report "keep_if" do
    image_map.keep_if {|key, value| key.to_s.match?(/img([1-9]|[1-9][0-9]{1,3}|[1-4][0-9]{4}|50000)_url/) }.each_key do |url_key|  
      Struct.new(:url, :caption, :title).new(url_key, url_key.to_s.gsub(/url/, "caption"), url_key.to_s.gsub(/url/, "title"))
      result_count[:keep_if] += 1
    end 
  end
  row.report "select" do
    image_map.select {|key, value| key.to_s.match?(/img([1-9]|[1-9][0-9]{1,3}|[1-4][0-9]{4}|50000)_url/) }.each_key do |url_key|  
      Struct.new(:url, :caption, :title).new(url_key, url_key.to_s.gsub(/url/, "caption"), url_key.to_s.gsub(/url/, "title"))
      result_count[:select] += 1
    end 
  end
end

result_count
=> {:forEach=>50000, :keep_if=>50000, :select=>50000}

計測結果

1回目

レポート名称  user system total real
forEach 1.257482 0.099606 1.357088 ( 1.371853)
keep_if 2.062804 0.068399 2.131203 ( 2.144689)
select 2.317476 0.111707 2.429183 ( 2.436765)

2回目

レポート名称  user system total real
forEach 1.292436 0.028099 1.320535 ( 1.325629)
keep_if 1.595292 0.042578 1.637870 ( 1.646557)
select 1.285199 0.022699 1.307898 ( 1.317835)

3回目

レポート名称  user system total real
forEach 1.609683 0.048782 1.658465 ( 1.667256)
keep_if 1.148458 0.017314 1.165772 ( 1.171699)
select 1.340724 0.039342 1.380066 ( 1.384263)

4回目

レポート名称  user system total real
forEach 1.439700 0.041089 1.480789 ( 1.488726)
keep_if 1.220690 0.019430 1.240120 ( 1.252073)
select 1.331339 0.042890 1.374229 ( 1.379941)

5回目

レポート名称  user system total real
forEach 1.173899 0.010056 1.183955 ( 1.187291)
keep_if 1.328620 0.038243 1.366863 ( 1.378679)
select 1.267303 0.029949 1.297252 ( 1.314939)
  • パフォーマンスとしては大差ないが、可読性では後者となる誤差値が出てはいるものの、平均的にはkeep_if / selectの方が早い印象を受けなくもない(可読性では後者となる)
0
0
5

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