From ab8b12eaf625d7e7a1ebf589ff4f8dbf85b65b7c Mon Sep 17 00:00:00 2001 From: Ryuta Kamizono Date: Sat, 13 Jun 2020 14:36:22 +0900 Subject: [PATCH] PERF: 35% faster attributes for readonly usage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) ``` --- activemodel/lib/active_model/attribute.rb | 15 +++---- .../lib/active_model/attribute_set/builder.rb | 42 +++++++++++++++---- 2 files changed, 43 insertions(+), 14 deletions(-) diff --git a/activemodel/lib/active_model/attribute.rb b/activemodel/lib/active_model/attribute.rb index 42d00b7248..613d188c37 100644 --- a/activemodel/lib/active_model/attribute.rb +++ b/activemodel/lib/active_model/attribute.rb @@ -5,16 +5,16 @@ require "active_support/core_ext/object/duplicable" module ActiveModel class Attribute # :nodoc: class << self - def from_database(name, value, type) - FromDatabase.new(name, value, type) + def from_database(name, value_before_type_cast, type, value = nil) + FromDatabase.new(name, value_before_type_cast, type, nil, value) end - def from_user(name, value, type, original_attribute = nil) - FromUser.new(name, value, type, original_attribute) + def from_user(name, value_before_type_cast, type, original_attribute = nil) + FromUser.new(name, value_before_type_cast, type, original_attribute) end - def with_cast_value(name, value, type) - WithCastValue.new(name, value, type) + def with_cast_value(name, value_before_type_cast, type) + WithCastValue.new(name, value_before_type_cast, type) end def null(name) @@ -30,11 +30,12 @@ module ActiveModel # This method should not be called directly. # 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 @value_before_type_cast = value_before_type_cast @type = type @original_attribute = original_attribute + @value = value unless value.nil? end def value diff --git a/activemodel/lib/active_model/attribute_set/builder.rb b/activemodel/lib/active_model/attribute_set/builder.rb index eb92be692d..57df36d24a 100644 --- a/activemodel/lib/active_model/attribute_set/builder.rb +++ b/activemodel/lib/active_model/attribute_set/builder.rb @@ -14,11 +14,17 @@ module ActiveModel def build_from_database(values = {}, additional_types = {}) attributes = LazyAttributeHash.new(types, values, additional_types, default_attributes) - AttributeSet.new(attributes) + LazyAttributeSet.new(attributes) end end end + class LazyAttributeSet < AttributeSet # :nodoc: + def fetch_value(name, &block) + attributes.fetch_value(name, &block) + end + end + class LazyAttributeHash # :nodoc: delegate :transform_values, :each_key, :each_value, :fetch, :except, to: :materialize @@ -26,9 +32,10 @@ module ActiveModel @types = types @values = values @additional_types = additional_types - @materialized = false - @delegate_hash = delegate_hash @default_attributes = default_attributes + @delegate_hash = delegate_hash + @casted_values = {} + @materialized = false end def key?(key) @@ -43,6 +50,25 @@ module ActiveModel delegate_hash[key] = value 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 dup.tap do |copy| copy.instance_variable_set(:@delegate_hash, delegate_hash.transform_values(&:dup)) @@ -104,13 +130,15 @@ module ActiveModel private attr_reader :types, :values, :additional_types, :delegate_hash, :default_attributes - def assign_default_value(name) - type = additional_types.fetch(name, types[name]) - value_present = true + def assign_default_value( + name, + value_present = true, value = values.fetch(name) { value_present = false } + ) + type = additional_types.fetch(name, types[name]) 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) attr = default_attributes[name] if attr