Micro-optimize ActiveRecord::Core#hash

Avoids calling _read_attribute("id") more than necessary

```ruby
require "active_record"
require "benchmark/ips"

ActiveRecord::Base.establish_connection(adapter: "sqlite3", database:  ":memory:")
ActiveRecord::Base.connection.create_table :users do |t|
  t.text :name
end

class User < ActiveRecord::Base
  # This is the current implementation from ActiveRecord::Core
  def hash
    if id
      self.class.hash ^ id.hash
    else
      super
    end
  end
end
class UserFastHash < ActiveRecord::Base
  self.table_name = "users"
  def hash
    if i = id
      self.class.hash ^ i.hash
    else
      super
    end
  end
end

1_000.times { |i| User.create(name: "test #{i}") }
slow_users = User.take(1000)
fast_users = UserFastHash.take(1000)

Benchmark.ips do |x|
  x.report("slowhash") {
    hash = {}
    slow_users.each { |u| hash[u] = 1 }
  }
  x.report("fasthash") {
    hash = {}
    fast_users.each { |u| hash[u] = 1 }
  }
  x.compare!
end
```

```
Warming up --------------------------------------
            slowhash   129.000  i/100ms
            fasthash   177.000  i/100ms
Calculating -------------------------------------
            slowhash      1.307k (± 0.7%) i/s -      6.579k in   5.033141s
            fasthash      1.764k (± 2.4%) i/s -      8.850k in   5.021749s

Comparison:
            fasthash:     1763.5 i/s
            slowhash:     1307.2 i/s - 1.35x  (± 0.00) slower
```
This commit is contained in:
Jonathan del Strother 2021-07-07 11:02:40 +01:00
parent 7373b5819a
commit 0c25a0baee
No known key found for this signature in database
GPG Key ID: 39C57B40653AA4FB

@ -601,6 +601,8 @@ def ==(comparison_object)
# Delegates to id in order to allow two records of the same type and id to work with something like:
# [ Person.find(1), Person.find(2), Person.find(3) ] & [ Person.find(1), Person.find(4) ] # => [ Person.find(1) ]
def hash
id = self.id
if id
self.class.hash ^ id.hash
else