PERF: 35% faster attributes for readonly usage

Instantiating attributes hash from raw database values is one of the
slower part of attributes.

Why that is necessary is to detect mutations. In other words, that isn't
necessary until mutations are happened.

`LazyAttributeHash` which was introduced at 0f29c21 is to instantiate
attribute lazily until first accessing the attribute (i.e.
`Model.find(1)` isn't slow yet, but `Model.find(1).attr_name` is still
slow).

This introduces `LazyAttributeSet` to instantiate attribute more lazily,
it doesn't instantiate attribute until first assigning/dirty checking
the attribute (i.e. `Model.find(1).attr_name` is no longer slow).

It makes attributes access about 35% faster for readonly (non-mutation)
usage.

https://gist.github.com/kamipo/4002c96a02859d8fe6503e26d7be4ad8

Before:

```
IPS
Warming up --------------------------------------
    attribute access     1.000  i/100ms
Calculating -------------------------------------
    attribute access      3.444  (± 0.0%) i/s -     18.000  in   5.259030s
MEMORY
Calculating -------------------------------------
    attribute access    38.902M memsize (     0.000  retained)
                       350.044k objects (     0.000  retained)
                        15.000  strings (     0.000  retained)
```

After (with `immutable_strings_by_default = true`):

```
IPS
Warming up --------------------------------------
    attribute access     1.000  i/100ms
Calculating -------------------------------------
    attribute access      4.652  (±21.5%) i/s -     23.000  in   5.034853s
MEMORY
Calculating -------------------------------------
    attribute access    27.782M memsize (     0.000  retained)
                       170.044k objects (     0.000  retained)
                        15.000  strings (     0.000  retained)
```
This commit is contained in:
Ryuta Kamizono 2020-06-13 14:36:22 +09:00
parent 00c2d3e1e6
commit ab8b12eaf6
2 changed files with 43 additions and 14 deletions

@ -5,16 +5,16 @@
module ActiveModel module ActiveModel
class Attribute # :nodoc: class Attribute # :nodoc:
class << self class << self
def from_database(name, value, type) def from_database(name, value_before_type_cast, type, value = nil)
FromDatabase.new(name, value, type) FromDatabase.new(name, value_before_type_cast, type, nil, value)
end end
def from_user(name, value, type, original_attribute = nil) def from_user(name, value_before_type_cast, type, original_attribute = nil)
FromUser.new(name, value, type, original_attribute) FromUser.new(name, value_before_type_cast, type, original_attribute)
end end
def with_cast_value(name, value, type) def with_cast_value(name, value_before_type_cast, type)
WithCastValue.new(name, value, type) WithCastValue.new(name, value_before_type_cast, type)
end end
def null(name) def null(name)
@ -30,11 +30,12 @@ def uninitialized(name, type)
# This method should not be called directly. # This method should not be called directly.
# Use #from_database or #from_user # Use #from_database or #from_user
def initialize(name, value_before_type_cast, type, original_attribute = nil) def initialize(name, value_before_type_cast, type, original_attribute = nil, value = nil)
@name = name @name = name
@value_before_type_cast = value_before_type_cast @value_before_type_cast = value_before_type_cast
@type = type @type = type
@original_attribute = original_attribute @original_attribute = original_attribute
@value = value unless value.nil?
end end
def value def value

@ -14,11 +14,17 @@ def initialize(types, default_attributes = {})
def build_from_database(values = {}, additional_types = {}) def build_from_database(values = {}, additional_types = {})
attributes = LazyAttributeHash.new(types, values, additional_types, default_attributes) attributes = LazyAttributeHash.new(types, values, additional_types, default_attributes)
AttributeSet.new(attributes) LazyAttributeSet.new(attributes)
end end
end end
end end
class LazyAttributeSet < AttributeSet # :nodoc:
def fetch_value(name, &block)
attributes.fetch_value(name, &block)
end
end
class LazyAttributeHash # :nodoc: class LazyAttributeHash # :nodoc:
delegate :transform_values, :each_key, :each_value, :fetch, :except, to: :materialize delegate :transform_values, :each_key, :each_value, :fetch, :except, to: :materialize
@ -26,9 +32,10 @@ def initialize(types, values, additional_types, default_attributes, delegate_has
@types = types @types = types
@values = values @values = values
@additional_types = additional_types @additional_types = additional_types
@materialized = false
@delegate_hash = delegate_hash
@default_attributes = default_attributes @default_attributes = default_attributes
@delegate_hash = delegate_hash
@casted_values = {}
@materialized = false
end end
def key?(key) def key?(key)
@ -43,6 +50,25 @@ def []=(key, value)
delegate_hash[key] = value delegate_hash[key] = value
end end
def fetch_value(name, &block)
if attr = delegate_hash[name]
return attr.value(&block)
end
@casted_values.fetch(name) do
value_present = true
value = values.fetch(name) { value_present = false }
if value_present
type = additional_types.fetch(name, types[name])
@casted_values[name] = type.deserialize(value)
else
attr = assign_default_value(name, value_present, value) || Attribute.null(name)
attr.value(&block)
end
end
end
def deep_dup def deep_dup
dup.tap do |copy| dup.tap do |copy|
copy.instance_variable_set(:@delegate_hash, delegate_hash.transform_values(&:dup)) copy.instance_variable_set(:@delegate_hash, delegate_hash.transform_values(&:dup))
@ -104,13 +130,15 @@ def materialize
private private
attr_reader :types, :values, :additional_types, :delegate_hash, :default_attributes attr_reader :types, :values, :additional_types, :delegate_hash, :default_attributes
def assign_default_value(name) def assign_default_value(
type = additional_types.fetch(name, types[name]) name,
value_present = true value_present = true,
value = values.fetch(name) { value_present = false } value = values.fetch(name) { value_present = false }
)
type = additional_types.fetch(name, types[name])
if value_present if value_present
delegate_hash[name] = Attribute.from_database(name, value, type) delegate_hash[name] = Attribute.from_database(name, value, type, @casted_values[name])
elsif types.key?(name) elsif types.key?(name)
attr = default_attributes[name] attr = default_attributes[name]
if attr if attr