提交 796cab45 编写于 作者: P Pete Higgins

Reduce allocations when running AR callbacks.

Inspired by @tenderlove's work in
c363fff2, this reduces the number of
strings allocated when running callbacks for ActiveRecord instances. I
measured that using this script:

```
require 'objspace'
require 'active_record'
require 'allocation_tracer'

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

ActiveRecord::Base.connection.instance_eval do
  create_table(:articles) { |t| t.string :name }
end

class Article < ActiveRecord::Base; end
a = Article.create name: "foo"
a = Article.find a.id

N = 10
result = ObjectSpace::AllocationTracer.trace do
  N.times { Article.find a.id }
end

result.sort.each do |k,v|
  p k => v
end
puts "total: #{result.values.map(&:first).inject(:+)}"
```

When I run this against master and this branch I get this output:

```
pete@balloon:~/projects/rails/activerecord$ git checkout master
M Gemfile
Switched to branch 'master'
pete@balloon:~/projects/rails/activerecord$ bundle exec ruby benchmark_allocation_with_callback_send.rb > allocations_before
pete@balloon:~/projects/rails/activerecord$ git checkout remove-dynamic-send-on-built-in-callbacks
M Gemfile
Switched to branch 'remove-dynamic-send-on-built-in-callbacks'
pete@balloon:~/projects/rails/activerecord$ bundle exec ruby benchmark_allocation_with_callback_send.rb > allocations_after
pete@balloon:~/projects/rails/activerecord$ diff allocations_before allocations_after
39d38
<
{["/home/pete/projects/rails/activesupport/lib/active_support/callbacks.rb",
81]=>[40, 0, 0, 0, 0, 0]}
42c41
< total: 630
---
> total: 590

```

In addition to this, there are two micro-optimizations present:

* Using `block.call if block` vs `yield if block_given?` when the block was being captured already.

```
pete@balloon:~/projects$ cat benchmark_block_call_vs_yield.rb
require 'benchmark/ips'

def block_capture_with_yield &block
  yield if block_given?
end

def block_capture_with_call &block
  block.call if block
end

def no_block_capture
  yield if block_given?
end

Benchmark.ips do |b|
  b.report("block_capture_with_yield") { block_capture_with_yield }
  b.report("block_capture_with_call") { block_capture_with_call }
  b.report("no_block_capture") { no_block_capture }
end
pete@balloon:~/projects$ ruby benchmark_block_call_vs_yield.rb
Calculating -------------------------------------
block_capture_with_yield
                        124979 i/100ms
block_capture_with_call
                        138340 i/100ms
    no_block_capture    136827 i/100ms
-------------------------------------------------
block_capture_with_yield
                      5703108.9 (±2.4%) i/s -   28495212 in   4.999368s
block_capture_with_call
                      6840730.5 (±3.6%) i/s -   34169980 in   5.002649s
    no_block_capture  5821141.4 (±2.8%) i/s -   29144151 in   5.010580s
```

* Defining and calling methods instead of using send.

```
pete@balloon:~/projects$ cat benchmark_method_call_vs_send.rb
require 'benchmark/ips'

class Foo
  def tacos
    nil
  end
end

my_foo = Foo.new

Benchmark.ips do |b|
  b.report('send') { my_foo.send('tacos') }
  b.report('call') { my_foo.tacos }
end
pete@balloon:~/projects$ ruby benchmark_method_call_vs_send.rb
Calculating -------------------------------------
                send     97736 i/100ms
                call    151142 i/100ms
-------------------------------------------------
                send  2683730.3 (±2.8%) i/s -   13487568 in   5.029763s
                call  8005963.9 (±2.7%) i/s -   40052630 in   5.006604s
```

The result of this is making typical ActiveRecord operations slightly faster:

https://gist.github.com/phiggins/e46e51dcc7edb45b5f98
上级 3a9b3ba0
......@@ -390,7 +390,7 @@ def invalid?(context = nil)
protected
def run_validations! #:nodoc:
run_callbacks :validate
run_validate_callbacks
errors.empty?
end
end
......
......@@ -108,7 +108,7 @@ def after_validation(*args, &block)
# Overwrite run validations to include callbacks.
def run_validations! #:nodoc:
run_callbacks(:validation) { super }
run_validation_callbacks { super }
end
end
end
......
......@@ -159,7 +159,7 @@ def delete_records(records, method)
count = scope.destroy_all.length
else
scope.to_a.each do |record|
record.run_callbacks :destroy
record.run_destroy_callbacks
end
arel = scope.arel
......
......@@ -289,25 +289,25 @@ module ClassMethods
end
def destroy #:nodoc:
run_callbacks(:destroy) { super }
run_destroy_callbacks { super }
end
def touch(*) #:nodoc:
run_callbacks(:touch) { super }
run_touch_callbacks { super }
end
private
def create_or_update #:nodoc:
run_callbacks(:save) { super }
run_save_callbacks { super }
end
def _create_record #:nodoc:
run_callbacks(:create) { super }
run_create_callbacks { super }
end
def _update_record(*) #:nodoc:
run_callbacks(:update) { super }
run_update_callbacks { super }
end
end
end
......@@ -360,7 +360,7 @@ def checkin(conn)
synchronize do
owner = conn.owner
conn.run_callbacks :checkin do
conn.run_checkin_callbacks do
conn.expire
end
......@@ -449,7 +449,7 @@ def checkout_new_connection
end
def checkout_and_verify(c)
c.run_callbacks :checkout do
c.run_checkout_callbacks do
c.verify!
end
c
......
......@@ -272,7 +272,7 @@ def initialize(attributes = nil, options = {})
init_attributes(attributes, options) if attributes
yield self if block_given?
run_callbacks :initialize unless _initialize_callbacks.empty?
run_initialize_callbacks
end
# Initialize an empty model object from +coder+. +coder+ must contain
......@@ -294,8 +294,8 @@ def init_with(coder)
self.class.define_attribute_methods
run_callbacks :find
run_callbacks :initialize
run_find_callbacks
run_initialize_callbacks
self
end
......@@ -331,7 +331,7 @@ def initialize_dup(other) # :nodoc:
@attributes = @attributes.dup
@attributes.reset(self.class.primary_key)
run_callbacks(:initialize) unless _initialize_callbacks.empty?
run_initialize_callbacks
@aggregation_cache = {}
@association_cache = {}
......
......@@ -309,7 +309,7 @@ def rollback_active_record_state!
# Ensure that it is not called if the object was never persisted (failed create),
# but call it after the commit of a destroyed object.
def committed!(should_run_callbacks = true) #:nodoc:
run_callbacks :commit if should_run_callbacks && destroyed? || persisted?
run_commit_callbacks if should_run_callbacks && destroyed? || persisted?
ensure
force_clear_transaction_record_state
end
......@@ -317,7 +317,7 @@ def committed!(should_run_callbacks = true) #:nodoc:
# Call the +after_rollback+ callbacks. The +force_restore_state+ argument indicates if the record
# state should be rolled back to the beginning or just to the last savepoint.
def rolledback!(force_restore_state = false, should_run_callbacks = true) #:nodoc:
run_callbacks :rollback if should_run_callbacks
run_rollback_callbacks if should_run_callbacks
ensure
restore_transaction_record_state(force_restore_state)
clear_transaction_record_state
......
......@@ -78,18 +78,21 @@ module Callbacks
# save
# end
def run_callbacks(kind, &block)
cbs = send("_#{kind}_callbacks")
if cbs.empty?
yield if block_given?
send "run_#{kind}_callbacks", &block
end
private
def _run_callbacks(callbacks, &block)
if callbacks.empty?
block.call if block
else
runner = cbs.compile
runner = callbacks.compile
e = Filters::Environment.new(self, false, nil, block)
runner.call(e).value
end
end
private
# A hook invoked every time a before callback is halted.
# This can be overridden in AS::Callback implementors in order
# to provide better debugging/logging.
......@@ -722,6 +725,12 @@ def define_callbacks(*names)
names.each do |name|
class_attribute "_#{name}_callbacks"
set_callbacks name, CallbackChain.new(name, options)
module_eval <<-RUBY, __FILE__, __LINE__ + 1
def run_#{name}_callbacks(&block)
_run_callbacks(_#{name}_callbacks, &block)
end
RUBY
end
end
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册