Skip to content

Commit

Permalink
Optimize CurrentAttributes method generation
Browse files Browse the repository at this point in the history
The bulk of the optimization is to generate code rather than use
`define_method` with a closure.

```
Warming up --------------------------------------
            original   207.468k i/100ms
      code-generator   340.849k i/100ms
Calculating -------------------------------------
            original      2.127M (± 1.1%) i/s -     10.788M in   5.073860s
      code-generator      3.426M (± 0.9%) i/s -     17.383M in   5.073965s

Comparison:
      code-generator:  3426241.0 i/s
            original:  2126539.2 i/s - 1.61x  (± 0.00) slower
```

```ruby

require 'benchmark/ips'
require 'active_support/all'

class Original < ActiveSupport::CurrentAttributes
  attribute :foo
end

class CodeGen < ActiveSupport::CurrentAttributes
  class << self
    def attribute(*names)
      ActiveSupport::CodeGenerator.batch(generated_attribute_methods, __FILE__, __LINE__) do |owner|
        names.each do |name|
          owner.define_cached_method(name, namespace: :current_attributes) do |batch|
            batch <<
              "def #{name}" <<
              "attributes[:#{name}]" <<
              "end"
          end
          owner.define_cached_method("#{name}=", namespace: :current_attributes) do |batch|
            batch <<
              "def #{name}=(value)" <<
              "attributes[:#{name}] = value" <<
              "end"
          end
        end
      end

      ActiveSupport::CodeGenerator.batch(singleton_class, __FILE__, __LINE__) do |owner|
        names.each do |name|
          owner.define_cached_method(name, namespace: :current_attributes_delegation) do |batch|
            batch <<
              "def #{name}" <<
              "instance.#{name}" <<
              "end"
          end
          owner.define_cached_method("#{name}=", namespace: :current_attributes_delegation) do |batch|
            batch <<
              "def #{name}=(value)" <<
              "instance.#{name} = value" <<
              "end"
          end
        end
      end
    end
  end
  attribute :foo
end

Benchmark.ips do |x|
  x.report('original') { Original.foo }
  x.report('code-generator') { CodeGen.foo }
  x.compare!
end
```
  • Loading branch information
byroot committed Nov 2, 2021
1 parent f314bae commit bf33510
Show file tree
Hide file tree
Showing 4 changed files with 94 additions and 79 deletions.
69 changes: 3 additions & 66 deletions activemodel/lib/active_model/attribute_methods.rb
Expand Up @@ -208,7 +208,7 @@ def attribute_method_affix(*affixes)
# person.nickname_short? # => true
def alias_attribute(new_name, old_name)
self.attribute_aliases = attribute_aliases.merge(new_name.to_s => old_name.to_s)
CodeGenerator.batch(self, __FILE__, __LINE__) do |code_generator|
ActiveSupport::CodeGenerator.batch(self, __FILE__, __LINE__) do |code_generator|
attribute_method_matchers.each do |matcher|
method_name = matcher.method_name(new_name).to_s
target_name = matcher.method_name(old_name).to_s
Expand Down Expand Up @@ -274,7 +274,7 @@ def attribute_alias(name)
# end
# end
def define_attribute_methods(*attr_names)
CodeGenerator.batch(generated_attribute_methods, __FILE__, __LINE__) do |owner|
ActiveSupport::CodeGenerator.batch(generated_attribute_methods, __FILE__, __LINE__) do |owner|
attr_names.flatten.each { |attr_name| define_attribute_method(attr_name, _owner: owner) }
end
end
Expand Down Expand Up @@ -309,7 +309,7 @@ def define_attribute_methods(*attr_names)
# person.name # => "Bob"
# person.name_short? # => true
def define_attribute_method(attr_name, _owner: generated_attribute_methods)
CodeGenerator.batch(_owner, __FILE__, __LINE__) do |owner|
ActiveSupport::CodeGenerator.batch(_owner, __FILE__, __LINE__) do |owner|
attribute_method_matchers.each do |matcher|
method_name = matcher.method_name(attr_name)

Expand Down Expand Up @@ -358,69 +358,6 @@ def undefine_attribute_methods
end

private
class CodeGenerator # :nodoc:
class MethodSet
METHOD_CACHES = Hash.new { |h, k| h[k] = Module.new }

def initialize(namespace)
@cache = METHOD_CACHES[namespace]
@sources = []
@methods = {}
end

def define_cached_method(name, as: name)
name = name.to_sym
as = as.to_sym
@methods.fetch(name) do
unless @cache.method_defined?(as)
yield @sources
end
@methods[name] = as
end
end

def apply(owner, path, line)
unless @sources.empty?
@cache.module_eval("# frozen_string_literal: true\n" + @sources.join(";"), path, line)
end
@methods.each do |name, as|
owner.define_method(name, @cache.instance_method(as))
end
end
end

class << self
def batch(owner, path, line)
if owner.is_a?(CodeGenerator)
yield owner
else
instance = new(owner, path, line)
result = yield instance
instance.execute
result
end
end
end

def initialize(owner, path, line)
@owner = owner
@path = path
@line = line
@namespaces = Hash.new { |h, k| h[k] = MethodSet.new(k) }
end

def define_cached_method(name, namespace:, as: name, &block)
@namespaces[namespace].define_cached_method(name, as: as, &block)
end

def execute
@namespaces.each_value do |method_set|
method_set.apply(@owner, @path, @line - 1)
end
end
end
private_constant :CodeGenerator

def generated_attribute_methods
@generated_attribute_methods ||= Module.new.tap { |mod| include mod }
end
Expand Down
1 change: 1 addition & 0 deletions activesupport/lib/active_support.rb
Expand Up @@ -34,6 +34,7 @@ module ActiveSupport
extend ActiveSupport::Autoload

autoload :Concern
autoload :CodeGenerator
autoload :ActionableError
autoload :ConfigurationFile
autoload :CurrentAttributes
Expand Down
65 changes: 65 additions & 0 deletions activesupport/lib/active_support/code_generator.rb
@@ -0,0 +1,65 @@
# frozen_string_literal: true

module ActiveSupport
class CodeGenerator # :nodoc:
class MethodSet
METHOD_CACHES = Hash.new { |h, k| h[k] = Module.new }

def initialize(namespace)
@cache = METHOD_CACHES[namespace]
@sources = []
@methods = {}
end

def define_cached_method(name, as: name)
name = name.to_sym
as = as.to_sym
@methods.fetch(name) do
unless @cache.method_defined?(as)
yield @sources
end
@methods[name] = as
end
end

def apply(owner, path, line)
unless @sources.empty?
@cache.module_eval("# frozen_string_literal: true\n" + @sources.join(";"), path, line)
end
@methods.each do |name, as|
owner.define_method(name, @cache.instance_method(as))
end
end
end

class << self
def batch(owner, path, line)
if owner.is_a?(CodeGenerator)
yield owner
else
instance = new(owner, path, line)
result = yield instance
instance.execute
result
end
end
end

def initialize(owner, path, line)
@owner = owner
@path = path
@line = line
@namespaces = Hash.new { |h, k| h[k] = MethodSet.new(k) }
end

def define_cached_method(name, namespace:, as: name, &block)
@namespaces[namespace].define_cached_method(name, as: as, &block)
end

def execute
@namespaces.each_value do |method_set|
method_set.apply(@owner, @path, @line - 1)
end
end
end
end
38 changes: 25 additions & 13 deletions activesupport/lib/active_support/current_attributes.rb
Expand Up @@ -98,25 +98,37 @@ def instance

# Declares one or more attributes that will be given both class and instance accessor methods.
def attribute(*names)
generated_attribute_methods.module_eval do
ActiveSupport::CodeGenerator.batch(generated_attribute_methods, __FILE__, __LINE__) do |owner|
names.each do |name|
define_method(name) do
attributes[name.to_sym]
owner.define_cached_method(name, namespace: :current_attributes) do |batch|
batch <<
"def #{name}" <<
"attributes[:#{name}]" <<
"end"
end

define_method("#{name}=") do |attribute|
attributes[name.to_sym] = attribute
owner.define_cached_method("#{name}=", namespace: :current_attributes) do |batch|
batch <<
"def #{name}=(value)" <<
"attributes[:#{name}] = value" <<
"end"
end
end
end

names.each do |name|
define_singleton_method(name) do
instance.public_send(name)
end

define_singleton_method("#{name}=") do |attribute|
instance.public_send("#{name}=", attribute)
ActiveSupport::CodeGenerator.batch(singleton_class, __FILE__, __LINE__) do |owner|
names.each do |name|
owner.define_cached_method(name, namespace: :current_attributes_delegation) do |batch|
batch <<
"def #{name}" <<
"instance.#{name}" <<
"end"
end
owner.define_cached_method("#{name}=", namespace: :current_attributes_delegation) do |batch|
batch <<
"def #{name}=(value)" <<
"instance.#{name} = value" <<
"end"
end
end
end
end
Expand Down

0 comments on commit bf33510

Please sign in to comment.