Skip to content

Commit 1b94c81

Browse files
committed
class_attribute: reduce reliance on define_method
Full benchmark: https://gist.github.com/casperisfine/539e310eadd0cb650fc50ca0d9782569 Followup: rails#37903 Instead of always redefining a bmethod on assignment, if the reciever already has its own bmethod, we just update the local variable. This wasn't really considered before because class attributes are mostly meant for configuration, so not really expected to be changed at runtime, but it can happen. This is 10 to 17x faster to assign a class attributes: ``` interpreter: old: 1834149.6 i/s new: 17722346.6 i/s - 9.66x faster yjit: old: 2561173.8 i/s new: 43004422.5 i/s - 16.79x faster ``` The difference is even bigger if something is redefining `method_added` events, e.g. sorbet... All other cases are unchanged in performance, except definition that is marginally faster. One downside however is that generated accessors source location will no longer point to the `class_attribute` callsite.
1 parent f134705 commit 1b94c81

File tree

5 files changed

+34
-23
lines changed

5 files changed

+34
-23
lines changed

activerecord/test/cases/test_case.rb

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -141,9 +141,6 @@ def with_has_many_inversing(model = ActiveRecord::Base)
141141
yield
142142
ensure
143143
model.has_many_inversing = old
144-
if model != ActiveRecord::Base && !old
145-
model.singleton_class.remove_method(:has_many_inversing) # reset the class_attribute
146-
end
147144
end
148145

149146
def with_automatic_scope_inversing(*reflections)

activesupport/lib/active_support.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ module ActiveSupport
6262
autoload :Cache
6363
autoload :Callbacks
6464
autoload :Configurable
65+
autoload :ClassAttribute
6566
autoload :Deprecation
6667
autoload :Delegation
6768
autoload :Digest

activesupport/lib/active_support/callbacks.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ module Callbacks
6666

6767
included do
6868
extend ActiveSupport::DescendantsTracker
69-
class_attribute :__callbacks, instance_writer: false, default: {}
69+
class_attribute :__callbacks, instance_writer: false, instance_predicate: false, default: {}
7070
end
7171

7272
CALLBACK_FILTER_TYPES = [:before, :after, :around].freeze
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# frozen_string_literal: true
2+
3+
module ActiveSupport
4+
module ClassAttribute # :nodoc:
5+
class << self
6+
def redefine(owner, name, value)
7+
if owner.singleton_class?
8+
owner.redefine_method(name) { value }
9+
owner.silence_redefinition_of_method(name)
10+
end
11+
12+
owner.redefine_singleton_method(name) { value }
13+
owner.redefine_singleton_method("#{name}=") do |new_value|
14+
if owner.equal?(self)
15+
value = new_value
16+
else
17+
::ActiveSupport::ClassAttribute.redefine(self, name, new_value)
18+
end
19+
end
20+
end
21+
end
22+
end
23+
end

activesupport/lib/active_support/core_ext/class/attribute.rb

Lines changed: 9 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -91,24 +91,16 @@ def class_attribute(*attrs, instance_accessor: true,
9191
raise TypeError, "#{name.inspect} is not a symbol nor a string"
9292
end
9393

94-
class_methods << <<~RUBY # In case the method exists and is not public
95-
silence_redefinition_of_method def #{name}
96-
end
97-
RUBY
98-
99-
methods << <<~RUBY if instance_reader
100-
silence_redefinition_of_method def #{name}
101-
defined?(@#{name}) ? @#{name} : self.class.#{name}
102-
end
103-
RUBY
94+
name = name.to_sym
95+
::ActiveSupport::ClassAttribute.redefine(self, name, default)
10496

105-
class_methods << <<~RUBY
106-
silence_redefinition_of_method def #{name}=(value)
107-
redefine_method(:#{name}) { value } if singleton_class?
108-
redefine_singleton_method(:#{name}) { value }
109-
value
110-
end
111-
RUBY
97+
unless singleton_class?
98+
methods << <<~RUBY if instance_reader
99+
silence_redefinition_of_method def #{name}
100+
defined?(@#{name}) ? @#{name} : self.class.#{name}
101+
end
102+
RUBY
103+
end
112104

113105
methods << <<~RUBY if instance_writer
114106
silence_redefinition_of_method(:#{name}=)
@@ -125,7 +117,5 @@ def class_attribute(*attrs, instance_accessor: true,
125117

126118
location = caller_locations(1, 1).first
127119
class_eval(["class << self", *class_methods, "end", *methods].join(";").tr("\n", ";"), location.path, location.lineno)
128-
129-
attrs.each { |name| public_send("#{name}=", default) }
130120
end
131121
end

0 commit comments

Comments
 (0)