Skip to content

Conversation

tenderlove
Copy link
Member

We're trying to make instantiating models cheaper (for example doing Post.new), and discovered in the course of profiling that respond_to? is responsible for some amount of initialization allocations.

This patch changes Model.respond_to? to not allocate anymore which should help initialization performance (as well as other queries).

Here is the benchmark we used:

require "active_record"

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

ActiveRecord::Schema.define do
  create_table :posts, force: true do |t|
    t.string :title, default: "hello"
    t.text :body, default: "i am a blog post"
    t.string :author, default: "aaron"
    t.boolean :published, default: true
    t.integer :likes, default: 1000000
  end
end

class Post < ActiveRecord::Base
end

5.times { Post.new }

def m
  x = GC.stat(:total_allocated_objects)
  yield
  GC.stat(:total_allocated_objects) - x
end

allocs = m { 5000.times { Post.new } }
p ALLOCATIONS_PER_MODEL: (allocs / 5000)

allocs = m { 5000.times { Post.respond_to?(:default_scope) } }
p ALLOCATIONS_PER_RESPOND_TO: (allocs / 5000)

Before this patch:

$ bundle exec ruby -Iactiverecord/lib:~/git/vernier/lib test.rb
-- create_table(:posts, {force: true})
   -> 0.0045s
   {ALLOCATIONS_PER_MODEL: 9}
   {ALLOCATIONS_PER_RESPOND_TO: 2}

After this patch:

$ bundle exec ruby -Iactiverecord/lib:~/git/vernier/lib test.rb
-- create_table(:posts, {force: true})
   -> 0.0045s
   {ALLOCATIONS_PER_MODEL: 7}
   {ALLOCATIONS_PER_RESPOND_TO: 0}

@tenderlove
Copy link
Member Author

The railties tests seem to be failing on main as well 😵‍💫

Copy link
Member

@rafaelfranca rafaelfranca left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test should be fixed now. You can merge

We're trying to make instantiating models cheaper (for example doing
`Post.new`), and discovered in the course of profiling that
`respond_to?` is responsible for some amount of initialization
allocations.

This patch changes `Model.respond_to?` to not allocate anymore which
should help initialization performance (as well as other queries).

Here is the benchmark we used:

```ruby
require "active_record"

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

ActiveRecord::Schema.define do
  create_table :posts, force: true do |t|
    t.string :title, default: "hello"
    t.text :body, default: "i am a blog post"
    t.string :author, default: "aaron"
    t.boolean :published, default: true
    t.integer :likes, default: 1000000
  end
end

class Post < ActiveRecord::Base
end

5.times { Post.new }

def m
  x = GC.stat(:total_allocated_objects)
  yield
  GC.stat(:total_allocated_objects) - x
end

allocs = m { 5000.times { Post.new } }
p ALLOCATIONS_PER_MODEL: (allocs / 5000)

allocs = m { 5000.times { Post.respond_to?(:default_scope) } }
p ALLOCATIONS_PER_RESPOND_TO: (allocs / 5000)
```

Before this patch:

```
$ bundle exec ruby -Iactiverecord/lib:~/git/vernier/lib test.rb
-- create_table(:posts, {force: true})
   -> 0.0045s
   {ALLOCATIONS_PER_MODEL: 9}
   {ALLOCATIONS_PER_RESPOND_TO: 2}
```

After this patch:

```
$ bundle exec ruby -Iactiverecord/lib:~/git/vernier/lib test.rb
-- create_table(:posts, {force: true})
   -> 0.0045s
   {ALLOCATIONS_PER_MODEL: 7}
   {ALLOCATIONS_PER_RESPOND_TO: 0}
```

Co-Authored-By: Eileen M. Uchitelle <eileencodes@gmail.com>
@tenderlove tenderlove merged commit fa9cf26 into rails:main Jan 10, 2025
1 of 3 checks passed
@tenderlove tenderlove deleted the respond-to-alloc branch January 10, 2025 20:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants