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:
parent
00c2d3e1e6
commit
ab8b12eaf6
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user