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

Speed up homogeneous AR lists / reduce allocations

This commit speeds up allocating homogeneous lists of AR objects.  We
can know if the result set contains an STI column before initializing
every AR object, so this change pulls the "does this result set contain
an STI column?" test up, then uses a specialized instantiation function.
This way we only have to check for an STI column once rather than N
times.

This change also introduces a new initialization function that is meant
for use when allocating AR objects that come from the database.  Doing
this allows us to eliminate one hash allocation per AR instance.

Here is a benchmark:

```ruby
require 'active_record'
require 'benchmark/ips'

ActiveRecord::Base.establish_connection adapter: "sqlite3", database: ":memory:"

ActiveRecord::Migration.verbose = false

ActiveRecord::Schema.define do
  create_table :users, force: true do |t|
    t.string :name
    t.timestamps null: false
  end
end

class User < ActiveRecord::Base; end

2000.times do
  User.create!(name: "Gorby")
end

Benchmark.ips do |x|
  x.report("find") do
    User.limit(2000).to_a
  end
end
```

Results:

Before:

```
[aaron@TC activerecord (master)]$ be ruby -I lib:~/git/allocation_tracer/lib speed.rb
Warming up --------------------------------------
                find     5.000  i/100ms
Calculating -------------------------------------
                find     56.192  (± 3.6%) i/s -    285.000  in   5.080940s
```

After:

```
[aaron@TC activerecord (homogeneous-allocation)]$ be ruby -I lib:~/git/allocation_tracer/lib speed.rb
Warming up --------------------------------------
                find     7.000  i/100ms
Calculating -------------------------------------
                find     72.204  (± 2.8%) i/s -    364.000  in   5.044592s
```
This commit is contained in:
Aaron Patterson 2018-06-25 15:52:59 -07:00
parent 9858811825
commit 7d58fa87ef
No known key found for this signature in database
GPG key ID: 953170BCB4FFAFC6
5 changed files with 43 additions and 2 deletions

View file

@ -349,6 +349,26 @@ module ActiveRecord
self self
end end
##
# Initializer used for instantiating objects that have been read from the
# database. +attributes+ should be an attributes object, and unlike the
# `initialize` method, no assignment calls are made per attribute.
#
# :nodoc:
def init_from_db(attributes)
init_internals
@new_record = false
@attributes = attributes
yield self if block_given?
_run_find_callbacks
_run_initialize_callbacks
self
end
## ##
# :method: clone # :method: clone
# Identical to Ruby's clone method. This is a "shallow" copy. Be warned that your attributes are not copied. # Identical to Ruby's clone method. This is a "shallow" copy. Be warned that your attributes are not copied.

View file

@ -67,8 +67,14 @@ module ActiveRecord
# how this "single-table" inheritance mapping is implemented. # how this "single-table" inheritance mapping is implemented.
def instantiate(attributes, column_types = {}, &block) def instantiate(attributes, column_types = {}, &block)
klass = discriminate_class_for_record(attributes) klass = discriminate_class_for_record(attributes)
instantiate_instance_of(klass, attributes, column_types, &block)
end
# Given a class, an attributes hash, +instantiate+ returns a new instance
# of the class. Accepts only keys as strings.
def instantiate_instance_of(klass, attributes, column_types = {}, &block)
attributes = klass.attributes_builder.build_from_database(attributes, column_types) attributes = klass.attributes_builder.build_from_database(attributes, column_types)
klass.allocate.init_with("attributes" => attributes, "new_record" => false, &block) klass.allocate.init_from_db(attributes, &block)
end end
# Updates an object (or multiple objects) and saves it to the database, if validations pass. # Updates an object (or multiple objects) and saves it to the database, if validations pass.

View file

@ -49,7 +49,12 @@ module ActiveRecord
} }
message_bus.instrument("instantiation.active_record", payload) do message_bus.instrument("instantiation.active_record", payload) do
result_set.map { |record| instantiate(record, column_types, &block) } if result_set.includes_column?(inheritance_column)
result_set.map { |record| instantiate(record, column_types, &block) }
else
# Instantiate a homogeneous set
result_set.map { |record| instantiate_instance_of(self, record, column_types, &block) }
end
end end
end end

View file

@ -43,6 +43,11 @@ module ActiveRecord
@column_types = column_types @column_types = column_types
end end
# Does this result set include the column named +name+ ?
def includes_column?(name)
@columns.include? name
end
# Returns the number of elements in the rows array. # Returns the number of elements in the rows array.
def length def length
@rows.length @rows.length

View file

@ -12,6 +12,11 @@ module ActiveRecord
]) ])
end end
test "includes_column?" do
assert result.includes_column?("col_1")
assert_not result.includes_column?("foo")
end
test "length" do test "length" do
assert_equal 3, result.length assert_equal 3, result.length
end end