Skip to content

Implement object shapes for T_CLASS and T_MODULE #6637

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Oct 31, 2022

Conversation

jhawthorn
Copy link
Member

@jhawthorn jhawthorn commented Oct 26, 2022

Object Shapes were introduced in #6386 for T_OBJECT and "extended ivars" with the plan to later implement it for Classes and Modules, which previously were using an st_table. This is that change 😁

This brings a number of advantages over the previous approach, where each class/module had its own st_table: First it requires less storage, as we now store the values of the ivars in a flat array. This works particularly well with shapes because the shape can be reused between classes with the same ivar keys, which is very common (many classes only use the hidden __classpath__ and __autoload__ keys). Next, this unifies behaviour between all types of object which can hold ivars as everything now uses shapes (though there are small differences). Finally, this change allows using inline caches to fetch class instance variables.

One challenge while implementing this is that previously the same st_table *iv_tbl was shared between T_MODULE and the T_ICLASS representing that modules inheritance. This was convenient for class-variables (N.B. not class-instance-variables), but was incompatible with having IVs in an array (it previously relied on two levels of indirection because we may realloc storage when introducing new IVs). The first few commits of this PR remove this technique, and instead in the cases we needed the behaviour we look though the original module. (T_ICLASS never supported proper instance variables, only explicit access via the st_table).

RActor notes

One quirk of IVar access for RActors is that when on a non-main RActor we must guard that the result we receive is shareable. For this reason (and due to locking requirements described below) we don't attempt to use inline caches on other RActors, and simply fall back to the generic safe/locking implementation (which should already have similar or better performance to today).

Previously thread safety was provided using lock_st_lookup, lock_st_is_member, lock_st_insert, lock_st_delete, which hold the VM lock when performing their operation. Similarly most operations now use RB_VM_LOCK_ENTER/RB_VM_LOCK_LEAVE around their access of the iv storage. With two exceptions:

  • rb_ivar_defined does not need to perform any locking as all information is encoded in the immutable shape
  • When we read an instance variable using the inline cache from the main RActor, we do not perform locking, instead relying on the assumption that only main RActors are allowed to modify class ivars. (Non-main ractors aren't able to use inline caches per above)

It feels like in the future even more of these accesses could be made in a more lock-free way. Since shapes themselves are immutable, and we never shrink the IV storage in most cases it should be safe to access storage without locking (other than maybe removed ivars, which could be worked around), and likely could also be safe to set variables when it didn't require a shape transition (which would require some form of locking, but possibly much lighter or more granular), so long as free (specifically the free from realloc) was deferred until GC or some other safepoint. However none of this was explored or attempted because RActor's current use and the relative rarity of class instance variable usage didn't justify the complexity or risk of this approach.

Because of the locking requirement on class instance variable sets, this doesn't attempt use inline caches in that case (the inline cache is only used for read). If this ends up being common we could revisit this.

Benchmark

$ ruby --disable=gems -rrubygems -I./benchmark/lib ./benchmark/benchmark-driver/exe/benchmark-driver \
            --executables="ruby-3.1.2::/home/jhawthorn/.rubies/ruby-3.1.2/bin/ruby -I.ext/common --disable-gem" \
            --executables="ruby-trunk::/home/jhawthorn/.rubies/ruby-trunk/bin/ruby -I.ext/common --disable-gem" \
            --executables="built-ruby::./miniruby -I./lib -I. -I.ext/common  ./tool/runruby.rb --extout=.ext  -- --disable-gems --disable-gem" \
            --output=markdown --output-compare -v $(find ./benchmark -maxdepth 1 -name 'vm_ivar_of_class' -o -name '*vm_ivar_of_class*.yml' -o -name '*vm_ivar_of_class*.rb' | sort)
ruby-3.1.2: ruby 3.1.2p20 (2022-04-12 revision 4491bb740a) [x86_64-linux]
ruby-trunk: ruby 3.2.0dev (2022-10-26T05:49:35Z master df43611021) [x86_64-linux]
built-ruby: ruby 3.2.0dev (2022-10-26T05:56:39Z object-shapes-clas.. 84f88cbd9e) [x86_64-linux]
# Iteration per second (i/s)

|                      |ruby-3.1.2|ruby-trunk|built-ruby|
|:---------------------|---------:|---------:|---------:|
|vm_ivar_of_class_set  |    8.738M|    9.788M|   10.216M|
|                      |         -|     1.12x|     1.17x|
|vm_ivar_of_class      |    7.703M|    8.008M|   16.433M|
|                      |         -|     1.04x|     2.13x|

As expected we see a significant ~2x speedup from using inline caching.

cc @jemmaissroff @tenderlove

@jhawthorn
Copy link
Member Author

I tested in our largest application and this saves us 16MB of RAM through our classes and modules (I also spotted that we were previously double counting the st_table which I corrected for in my measurement).

Copy link
Contributor

@jemmaissroff jemmaissroff left a comment

Choose a reason for hiding this comment

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

This is very exciting, thanks for writing it!

@jhawthorn jhawthorn force-pushed the object-shapes-classes-squash3 branch from 84f88cb to 6e049d5 Compare October 31, 2022 20:11
@jhawthorn jhawthorn merged commit 02f1554 into ruby:master Oct 31, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants