From: eregontp@gmail.com
To: ruby-core@ruby-lang.org
Subject: [ruby-core:103125] [Ruby master Feature#17763] Implement cache for cvars
Date: Wed, 31 Mar 2021 10:15:13 +0000 (UTC) [thread overview]
Message-ID: <redmine.journal-91200.20210331101513.9236@ruby-lang.org> (raw)
In-Reply-To: redmine.issue-17763.20210330175734.9236@ruby-lang.org
Issue #17763 has been updated by Eregon (Benoit Daloze).
tenderlovemaking (Aaron Patterson) wrote in #note-6:
> Also in this case, it makes me feel weird to change the implementation of Rails when we can just make Ruby perform better. Changing Rails to suit Ruby seems like the tail wagging the dog (obviously not all cases are clear cut though)
I have a different view of this, before this change, class variables were always extremely slow compared to class instance variables (10x in the case of `ActiveRecord::Base.logger`).
So changing it in Rails would fix it for all Ruby versions.
The logger seems handled by `mattr_accessor`:
https://github.com/rails/rails/blob/d612542336d9a61381311c95a27d801bb4094779/activerecord/lib/active_record/core.rb#L20
And `mattr_accessor` is what uses class variables:
https://github.com/rails/rails/blob/d612542336d9a61381311c95a27d801bb4094779/activesupport/lib/active_support/core_ext/module/attribute_accessors.rb
But it seems like it could use an instance variable on the class instead, and use attr_accessor + for instance methods `def foo; self.class.foo` and the writer.
> but in other places we would have to basically re-implement class variable behavior (and at that point, you may as well use class variables).
That might be worth it for performance, i.e., using class methods + class instance variables instead of class variables.
Also https://github.com/rails/rails/blob/d612542336d9a61381311c95a27d801bb4094779/activesupport/lib/active_support/core_ext/class/attribute.rb#L85 seems to have some inheritance and yet already does not use class variables.
My personal point of view is class variables are de-facto deprecated syntax due to their strange and often unexpected semantics, and which about every Ruby tutorial/book recommends against using.
The least usages of class variables we have the better we are IMHO.
I once went through removing many class variables in the CRuby repo and it was fairly easy for most cases IIRC, in part because most of these cases did not want any inheritance.
---
To clarify, my point is this is good work and now that it's done and I think we should merge it,
but it seems worthwhile to look in Rails and other places if class variables are really needed.
If they can be replaced with class ivars, we could speed those accesses on all Ruby versions, not just 3.1+.
----------------------------------------
Feature #17763: Implement cache for cvars
https://bugs.ruby-lang.org/issues/17763#change-91200
* Author: eileencodes (Eileen Uchitelle)
* Status: Open
* Priority: Normal
----------------------------------------
# Introduce inline cache for class variable reads
@tenderlove and I would like to introduce an inline cache for class variable reads. We've attached a patch that introduces the cache. Class variable reads are popular in Rails applications for example, Active Record's `#logger`.
GitHub PR: https://github.com/ruby/ruby/pull/4340
## Cache Design
This patch introduces a hash table that's stored on the same class as the class variable value.
For example:
```ruby
class A
@@foo = 1
end
class B < A
def self.read_foo
@@foo
end
end
```
The above code stores the value for `@@foo` on the `A` class and stores an inline cache value on the `A` class as well. The instruction sequences for the `read_foo` method point at the CVAR inline cache entry stored on class `A`.
The lifecycle of these caches are similar to instance variable inline caches.
### Diagram of the cache:
![cvar cache](https://gist.githubusercontent.com/eileencodes/ddd95be978df27eb76543d352d516449/raw/13e969320159a4e1bff9444694a1ac198e892237/cvar%2520cache@2x%2520(6).png)
## Performance Characteristics
When class variables are read, Ruby needs to check each class in the inheritance tree to ensure that the class variable isn't set on any other classes in the tree. If the same cvar is set on a class in the inheritance tree then a "cvar overtaken" error will be raised.
Because of how cvar reads work, the more classes in the inheritance tree the more expensive a cvar read is. To demonstrate this here is a benchmark that reads a cvar from a class with 1 module, 30 modules, and 100 modules in the inheritance chain. On Ruby master 100 modules is 8.5x slower than including 1 module. With the cache, there is no performance difference between including 1 module and including 100 modules.
Benchmark script:
```ruby
require "benchmark/ips"
MODULES = ["B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", "AA", "BB", "CC", "DD", "EE", "FF", "GG", "HH", "II", "JJ", "KK", "LL", "MM", "NN", "OO", "PP", "QQ", "RR", "SS", "TT", "UU", "VV", "WW", "XX", "YY", "ZZ", "AAA", "BBB", "CCC", "DDD", "EEE", "FFF", "GGG", "HHH", "III", "JJJ", "KKK", "LLL", "MMM", "NNN", "OOO", "PPP", "QQQ", "RRR", "SSS", "TTT", "UUU", "VVV", "WWW", "XXX", "YYY", "ZZZ", "AAAA", "BBBB", "CCCC", "DDDD", "EEEE", "FFFF", "GGGG", "HHHH", "IIII", "JJJJ", "KKKK", "LLLL", "MMMM", "NNNN", "OOOO", "PPPP", "QQQQ", "RRRR", "SSSS", "TTTT", "UUUU", "VVVV", "WWWW"]
class A
@@foo = 1
def self.foo
@@foo
end
eval <<-EOM
module #{MODULES.first}
end
include #{MODULES.first}
EOM
end
class Athirty
@@foo = 1
def self.foo
@@foo
end
MODULES.take(30).each do |module_name|
eval <<-EOM
module #{module_name}
end
include #{module_name}
EOM
end
end
class Ahundred
@@foo = 1
def self.foo
@@foo
end
MODULES.each do |module_name|
eval <<-EOM
module #{module_name}
end
include #{module_name}
EOM
end
end
Benchmark.ips do |x|
x.report "1 module" do
A.foo
end
x.report "30 modules" do
Athirty.foo
end
x.report "100 modules" do
Ahundred.foo
end
x.compare!
end
```
Ruby 3.0 master:
```
Warming up --------------------------------------
1 module 1.231M i/100ms
30 modules 432.020k i/100ms
100 modules 145.399k i/100ms
Calculating -------------------------------------
1 module 12.210M (± 2.1%) i/s - 61.553M in 5.043400s
30 modules 4.354M (± 2.7%) i/s - 22.033M in 5.063839s
100 modules 1.434M (± 2.9%) i/s - 7.270M in 5.072531s
Comparison:
1 module: 12209958.3 i/s
30 modules: 4354217.8 i/s - 2.80x (± 0.00) slower
100 modules: 1434447.3 i/s - 8.51x (± 0.00) slower
```
Ruby 3.0 with cvar cache:
```
Warming up --------------------------------------
1 module 1.641M i/100ms
30 modules 1.655M i/100ms
100 modules 1.620M i/100ms
Calculating -------------------------------------
1 module 16.279M (± 3.8%) i/s - 82.038M in 5.046923s
30 modules 15.891M (± 3.9%) i/s - 79.459M in 5.007958s
100 modules 16.087M (± 3.6%) i/s - 81.005M in 5.041931s
Comparison:
1 module: 16279458.0 i/s
100 modules: 16087484.6 i/s - same-ish: difference falls within error
30 modules: 15891406.2 i/s - same-ish: difference falls within error
```
### Rails Application Benchmarks
We also benchmarked `ActiveRecord::Base.logger` since `logger` is a cvar and there are 63 modules in the inheritance chain. This is an example of a real-world improvement to Rails applications.
Benchmark:
```ruby
require "benchmark/ips"
require_relative "config/environment"
Benchmark.ips do |x|
x.report "logger" do
ActiveRecord::Base.logger
end
end
```
Ruby 3.0 master:
```
Warming up --------------------------------------
logger 155.251k i/100ms
Calculating -------------------------------------
```
Ruby 3.0 with cvar cache:
```
Warming up --------------------------------------
logger 1.546M i/100ms
Calculating -------------------------------------
logger 14.857M (± 4.8%) i/s - 74.198M in 5.006202s
```
We also measured database queries in Rails and with the cvar cache they are about ~9% faster.
Benchmark code:
```ruby
class BugTest < Minitest::Test
def test_association_stuff
post = Post.create!
Benchmark.ips do |x|
x.report "query" do
Post.first
end
end
end
end
```
Ruby 3.0 master / Rails 6.1:
```
Warming up --------------------------------------
query 790.000 i/100ms
Calculating -------------------------------------
query 7.601k (± 3.8%) i/s - 38.710k in 5.100534s
```
Ruby 3.0 cvar cache / Rails 6.1:
```
Warming up --------------------------------------
query 731.000 i/100ms
Calculating -------------------------------------
query 7.089k (± 3.3%) i/s - 35.819k in 5.058215s
```
--
https://bugs.ruby-lang.org/
Unsubscribe: <mailto:ruby-core-request@ruby-lang.org?subject=unsubscribe>
<http://lists.ruby-lang.org/cgi-bin/mailman/options/ruby-core>
next prev parent reply other threads:[~2021-03-31 10:15 UTC|newest]
Thread overview: 18+ messages / expand[flat|nested] mbox.gz Atom feed top
2021-03-30 17:57 [ruby-core:103105] [Ruby master Feature#17763] Implement cache for cvars eileencodes
2021-03-30 18:24 ` [ruby-core:103106] " eileencodes
2021-03-30 19:05 ` [ruby-core:103107] " eregontp
2021-03-30 19:26 ` [ruby-core:103109] " tenderlove
2021-03-30 19:45 ` [ruby-core:103111] " eregontp
2021-03-30 23:27 ` [ruby-core:103117] " duerst
2021-03-30 23:57 ` [ruby-core:103118] " tenderlove
2021-03-31 10:15 ` eregontp [this message]
2021-05-21 11:35 ` [ruby-core:103948] " eregontp
2021-05-21 12:58 ` [ruby-core:103949] " jean.boussier
2021-06-02 2:13 ` [ruby-core:104133] " ko1
2021-06-02 9:10 ` [ruby-core:104137] " jean.boussier
2021-06-04 17:46 ` [ruby-core:104167] " eileencodes
2021-06-04 17:47 ` [ruby-core:104168] " eileencodes
2021-06-09 0:31 ` [ruby-core:104210] " ko1
2021-06-11 17:27 ` [ruby-core:104236] " eileencodes
2021-06-17 7:19 ` [ruby-core:104336] " matz
2021-12-24 14:58 ` [ruby-core:106810] " ko1 (Koichi Sasada)
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-list from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
List information: https://www.ruby-lang.org/en/community/mailing-lists/
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=redmine.journal-91200.20210331101513.9236@ruby-lang.org \
--to=ruby-core@ruby-lang.org \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for read-only IMAP folder(s) and NNTP newsgroup(s).