PERF: 15% faster attribute access

Delegating to just one line method is to not be worth it.
Avoiding the delegation makes `read_attribute` about 15% faster.

```ruby
ActiveRecord::Schema.define do
  create_table :users, force: true do |t|
    t.string :name
  end
end

class User < ActiveRecord::Base
  def fast_read_attribute(attr_name, &block)
    name = attr_name.to_s
    name = self.class.attribute_aliases[name] || name

    name = @primary_key if name == "id" && @primary_key
    @attributes.fetch_value(name, &block)
  end
end

user = User.create!(name: "user name")

Benchmark.ips do |x|
  x.report("read_attribute('id')") { user.read_attribute('id') }
  x.report("read_attribute('name')") { user.read_attribute('name') }
  x.report("fast_read_attribute('id')") { user.fast_read_attribute('id') }
  x.report("fast_read_attribute('name')") { user.fast_read_attribute('name') }
end
```

```
Warming up --------------------------------------
read_attribute('id')   165.744k i/100ms
read_attribute('name')
                       162.229k i/100ms
fast_read_attribute('id')
                       192.543k i/100ms
fast_read_attribute('name')
                       191.209k i/100ms
Calculating -------------------------------------
read_attribute('id')      1.648M (± 1.7%) i/s -      8.287M in   5.030170s
read_attribute('name')
                          1.636M (± 3.9%) i/s -      8.274M in   5.065356s
fast_read_attribute('id')
                          1.918M (± 1.8%) i/s -      9.627M in   5.021271s
fast_read_attribute('name')
                          1.928M (± 0.9%) i/s -      9.752M in   5.058820s
```
This commit is contained in:
Ryuta Kamizono 2020-06-05 09:12:21 +09:00
parent b0231a7aa0
commit 27a1ca2bfe
5 changed files with 10 additions and 12 deletions

View File

@ -48,10 +48,12 @@ module ActiveModel
def write_from_user(name, value)
raise FrozenError, "can't modify frozen attributes" if frozen?
attributes[name] = self[name].with_value_from_user(value)
value
end
def write_cast_value(name, value)
attributes[name] = self[name].with_cast_value(value)
value
end
def freeze

View File

@ -48,7 +48,7 @@ module ActiveModel
) do |temp_method_name, attr_name_expr|
owner <<
"def #{temp_method_name}(value)" <<
" write_attribute(#{attr_name_expr}, value)" <<
" _write_attribute(#{attr_name_expr}, value)" <<
"end"
end
end
@ -124,12 +124,11 @@ module ActiveModel
name = attr_name.to_s
name = self.class.attribute_aliases[name] || name
_write_attribute(name, value)
@attributes.write_from_user(name, value)
end
def _write_attribute(attr_name, value)
@attributes.write_from_user(attr_name, value)
value
end
alias :attribute= :_write_attribute
@ -137,12 +136,11 @@ module ActiveModel
name = attr_name.to_s
name = self.class.attribute_aliases[name] || name
_read_attribute(name)
@attributes.fetch_value(name)
end
def _read_attribute(attr_name)
def attribute(attr_name)
@attributes.fetch_value(attr_name)
end
alias :attribute :_read_attribute
end
end

View File

@ -182,7 +182,7 @@ module ActiveRecord
def has_attribute?(attr_name)
attr_name = attr_name.to_s
attr_name = attribute_aliases[attr_name] || attr_name
_has_attribute?(attr_name)
attribute_types.key?(attr_name)
end
def _has_attribute?(attr_name) # :nodoc:
@ -256,7 +256,7 @@ module ActiveRecord
def has_attribute?(attr_name)
attr_name = attr_name.to_s
attr_name = self.class.attribute_aliases[attr_name] || attr_name
_has_attribute?(attr_name)
@attributes.key?(attr_name)
end
def _has_attribute?(attr_name) # :nodoc:

View File

@ -27,7 +27,7 @@ module ActiveRecord
name = self.class.attribute_aliases[name] || name
name = @primary_key if name == "id" && @primary_key
_read_attribute(name, &block)
@attributes.fetch_value(name, &block)
end
# This method exists to avoid the expensive primary_key check internally, without

View File

@ -31,14 +31,13 @@ module ActiveRecord
name = self.class.attribute_aliases[name] || name
name = @primary_key if name == "id" && @primary_key
_write_attribute(name, value)
@attributes.write_from_user(name, value)
end
# This method exists to avoid the expensive primary_key check internally, without
# breaking compatibility with the write_attribute API
def _write_attribute(attr_name, value) # :nodoc:
@attributes.write_from_user(attr_name, value)
value
end
alias :attribute= :_write_attribute
@ -47,7 +46,6 @@ module ActiveRecord
private
def write_attribute_without_type_cast(attr_name, value)
@attributes.write_cast_value(attr_name, value)
value
end
end
end