1
0
Fork 0
mirror of https://github.com/rails/rails.git synced 2022-11-09 12:12:34 -05:00

Don't allocate new strings in compiled attribute methods

This improves memory and performance without having to use symbols which
present DoS problems. Thanks @headius and @tenderlove for the
suggestion.

Benchmark
---------

require 'active_record'
require 'benchmark/ips'

ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database:
':memory:')

class Post < ActiveRecord::Base
  connection.create_table :posts, force: true do |t|
    t.string :name
  end
end

post = Post.create name: 'omg'

Benchmark.ips do |r|
  r.report('Post.new')          { Post.new name: 'omg' }
  r.report('post.name')         { post.name }
  r.report('post.name=')        { post.name = 'omg' }
  r.report('Post.find(1).name') { Post.find(1).name }
end

Before
------

Calculating -------------------------------------
            Post.new      1419 i/100ms
           post.name      7538 i/100ms
          post.name=      3024 i/100ms
   Post.find(1).name       243 i/100ms
-------------------------------------------------
            Post.new    20637.6 (±12.7%) i/s -     102168 in   5.039578s
           post.name  1167897.7 (±18.2%) i/s -    5186144 in   4.983077s
          post.name=    64305.6 (±9.6%) i/s -     317520 in   4.998720s
   Post.find(1).name     2678.8 (±10.8%) i/s -      13365 in   5.051265s

After
-----

Calculating -------------------------------------
            Post.new      1431 i/100ms
           post.name      7790 i/100ms
          post.name=      3181 i/100ms
   Post.find(1).name       245 i/100ms
-------------------------------------------------
            Post.new    21308.8 (±12.2%) i/s -     105894 in   5.053879s
           post.name  1534103.8 (±2.1%) i/s -    7634200 in   4.979405s
          post.name=    67441.0 (±7.5%) i/s -     337186 in   5.037871s
   Post.find(1).name     2681.9 (±10.6%) i/s -      13475 in   5.084511s
This commit is contained in:
Jon Leighton 2012-10-12 12:33:11 +01:00
parent 9e5f7cc62e
commit f1765019ce
2 changed files with 39 additions and 20 deletions

View file

@ -35,21 +35,36 @@ module ActiveRecord
protected protected
# We want to generate the methods via module_eval rather than define_method, # We want to generate the methods via module_eval rather than
# because define_method is slower on dispatch and uses more memory (because it # define_method, because define_method is slower on dispatch and
# creates a closure). # uses more memory (because it creates a closure).
# #
# But sometimes the database might return columns with characters that are not # But sometimes the database might return columns with
# allowed in normal method names (like 'my_column(omg)'. So to work around this # characters that are not allowed in normal method names (like
# we first define with the __temp__ identifier, and then use alias method to # 'my_column(omg)'. So to work around this we first define with
# rename it to what we want. # the __temp__ identifier, and then use alias method to rename
def define_method_attribute(attr_name) # it to what we want.
#
# We are also defining a constant to hold the frozen string of
# the attribute name. Using a constant means that we do not have
# to allocate an object on each call to the attribute method.
# Making it frozen means that it doesn't get duped when used to
# key the @attributes_cache in read_attribute.
def define_method_attribute(name)
safe_name = name.unpack('h*').first
generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1 generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1
def __temp__ module AttrNames
read_attribute('#{attr_name}') { |n| missing_attribute(n, caller) } unless defined? ATTR_#{safe_name}
ATTR_#{safe_name} = #{name.inspect}.freeze
end
end end
alias_method '#{attr_name}', :__temp__
undef_method :__temp__ def __temp__#{safe_name}
read_attribute(AttrNames::ATTR_#{safe_name}) { |n| missing_attribute(n, caller) }
end
alias_method #{name.inspect}, :__temp__#{safe_name}
undef_method :__temp__#{safe_name}
STR STR
end end

View file

@ -9,15 +9,19 @@ module ActiveRecord
module ClassMethods module ClassMethods
protected protected
def define_method_attribute=(attr_name)
if attr_name =~ ActiveModel::AttributeMethods::NAME_COMPILABLE_REGEXP # See define_method_attribute in read.rb for an explanation of
generated_attribute_methods.module_eval("def #{attr_name}=(new_value); write_attribute('#{attr_name}', new_value); end", __FILE__, __LINE__) # this code.
else def define_method_attribute=(name)
generated_attribute_methods.send(:define_method, "#{attr_name}=") do |new_value| safe_name = name.unpack('h*').first
write_attribute(attr_name, new_value) generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1
end def __temp__#{safe_name}=(value)
write_attribute(AttrNames::ATTR_#{safe_name}, value)
end end
end alias_method #{(name + '=').inspect}, :__temp__#{safe_name}=
undef_method :__temp__#{safe_name}=
STR
end
end end
# Updates the attribute identified by <tt>attr_name</tt> with the # Updates the attribute identified by <tt>attr_name</tt> with the