提交 0adcec49 编写于 作者: R Ryuta Kamizono

PERF: Avoid extra delegation to `LazyAttributeHash`

The extra delegation to `LazyAttributeHash` has non-negligible overhead.

Avoiding that delegation makes attributes access about 45% 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      5.066  (±19.7%) i/s -     25.000  in   5.024650s
MEMORY
Calculating -------------------------------------
    attribute access    27.382M memsize (     0.000  retained)
                       160.044k objects (     0.000  retained)
                        15.000  strings (     0.000  retained)
```
上级 ab8b12ea
......@@ -13,11 +13,11 @@ def initialize(attributes)
end
def [](name)
attributes[name] || Attribute.null(name)
@attributes[name] || default_attribute(name)
end
def []=(name, value)
attributes[name] = value
@attributes[name] = value
end
def values_before_type_cast
......@@ -25,9 +25,9 @@ def values_before_type_cast
end
def to_hash
initialized_attributes.transform_values(&:value)
keys.index_with { |name| self[name].value }
end
alias_method :to_h, :to_hash
alias :to_h :to_hash
def key?(name)
attributes.key?(name) && self[name].initialized?
......@@ -42,38 +42,36 @@ def fetch_value(name, &block)
end
def write_from_database(name, value)
attributes[name] = self[name].with_value_from_database(value)
@attributes[name] = self[name].with_value_from_database(value)
end
def write_from_user(name, value)
raise FrozenError, "can't modify frozen attributes" if frozen?
attributes[name] = self[name].with_value_from_user(value)
@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)
@attributes[name] = self[name].with_cast_value(value)
value
end
def freeze
@attributes.freeze
attributes.freeze
super
end
def deep_dup
self.class.allocate.tap do |copy|
copy.instance_variable_set(:@attributes, attributes.deep_dup)
end
AttributeSet.new(attributes.deep_dup)
end
def initialize_dup(_)
@attributes = attributes.dup
@attributes = @attributes.dup
super
end
def initialize_clone(_)
@attributes = attributes.clone
@attributes = @attributes.clone
super
end
......@@ -84,7 +82,7 @@ def reset(key)
end
def accessed
attributes.select { |_, attr| attr.has_been_read? }.keys
attributes.each_key.select { |name| self[name].has_been_read? }
end
def map(&block)
......@@ -100,8 +98,8 @@ def ==(other)
attr_reader :attributes
private
def initialized_attributes
attributes.select { |_, attr| attr.initialized? }
def default_attribute(name)
Attribute.null(name)
end
end
end
......@@ -13,45 +13,33 @@ def initialize(types, default_attributes = {})
end
def build_from_database(values = {}, additional_types = {})
attributes = LazyAttributeHash.new(types, values, additional_types, default_attributes)
LazyAttributeSet.new(attributes)
LazyAttributeSet.new(values, types, additional_types, default_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
def initialize(types, values, additional_types, default_attributes, delegate_hash = {})
@types = types
def initialize(values, types, additional_types, default_attributes, attributes = {})
super(attributes)
@values = values
@types = types
@additional_types = additional_types
@default_attributes = default_attributes
@delegate_hash = delegate_hash
@casted_values = {}
@materialized = false
end
def key?(key)
delegate_hash.key?(key) || values.key?(key) || types.key?(key)
def key?(name)
(values.key?(name) || types.key?(name) || @attributes.key?(name)) && self[name].initialized?
end
def [](key)
delegate_hash[key] || assign_default_value(key)
end
def []=(key, value)
delegate_hash[key] = value
def keys
keys = values.keys | types.keys | @attributes.keys
keys.keep_if { |name| self[name].initialized? }
end
def fetch_value(name, &block)
if attr = delegate_hash[name]
if attr = @attributes[name]
return attr.value(&block)
end
......@@ -63,12 +51,70 @@ def fetch_value(name, &block)
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 = default_attribute(name, value_present, value)
attr.value(&block)
end
end
end
protected
def attributes
unless @materialized
values.each_key { |key| self[key] }
types.each_key { |key| self[key] }
@materialized = true
end
@attributes
end
private
attr_reader :values, :types, :additional_types, :default_attributes
def default_attribute(
name,
value_present = true,
value = values.fetch(name) { value_present = false }
)
type = additional_types.fetch(name, types[name])
if value_present
@attributes[name] = Attribute.from_database(name, value, type, @casted_values[name])
elsif types.key?(name)
if attr = default_attributes[name]
@attributes[name] = attr.dup
else
@attributes[name] = Attribute.uninitialized(name, type)
end
else
Attribute.null(name)
end
end
end
class LazyAttributeHash # :nodoc:
delegate :transform_values, :each_value, :fetch, :except, to: :materialize
def initialize(types, values, additional_types, default_attributes, delegate_hash = {})
@types = types
@values = values
@additional_types = additional_types
@materialized = false
@delegate_hash = delegate_hash
@default_attributes = default_attributes
end
def key?(key)
delegate_hash.key?(key) || values.key?(key) || types.key?(key)
end
def [](key)
delegate_hash[key] || assign_default_value(key)
end
def []=(key, value)
delegate_hash[key] = value
end
def deep_dup
dup.tap do |copy|
copy.instance_variable_set(:@delegate_hash, delegate_hash.transform_values(&:dup))
......@@ -80,14 +126,9 @@ def initialize_dup(_)
super
end
def select
def each_key(&block)
keys = types.keys | values.keys | delegate_hash.keys
keys.each_with_object({}) do |key, hash|
attribute = self[key]
if yield(key, attribute)
hash[key] = attribute
end
end
keys.each(&block)
end
def ==(other)
......@@ -130,15 +171,13 @@ def materialize
private
attr_reader :types, :values, :additional_types, :delegate_hash, :default_attributes
def assign_default_value(
name,
value_present = true,
value = values.fetch(name) { value_present = false }
)
def assign_default_value(name)
type = additional_types.fetch(name, types[name])
value_present = true
value = values.fetch(name) { value_present = false }
if value_present
delegate_hash[name] = Attribute.from_database(name, value, type, @casted_values[name])
delegate_hash[name] = Attribute.from_database(name, value, type)
elsif types.key?(name)
attr = default_attributes[name]
if attr
......
......@@ -219,6 +219,12 @@ def assert_valid_value(*)
test "marshalling dump/load legacy materialized attribute hash" do
builder = AttributeSet::Builder.new(foo: Type::String.new)
def builder.build_from_database(values = {}, additional_types = {})
attributes = LazyAttributeHash.new(types, values, additional_types, default_attributes)
AttributeSet.new(attributes)
end
attributes = builder.build_from_database(foo: "1")
attributes.instance_variable_get(:@attributes).instance_eval do
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册