[ruby-core:114274] [Ruby master Feature#19783] Weak References in the GC

Issue #19783 has been reported by peterzhu2118 (Peter Zhu). ---------------------------------------- Feature #19783: Weak References in the GC https://bugs.ruby-lang.org/issues/19783 * Author: peterzhu2118 (Peter Zhu) * Status: Open * Priority: Normal ---------------------------------------- GitHub PR: https://github.com/ruby/ruby/pull/8113 I'm proposing support for weak references in the Ruby garbage collector. This feature adds a new function called `void rb_gc_mark_weak(VALUE *ptr)` which marks `*ptr` as weak, meaning that if no other object strongly marks `*ptr` (using `rb_gc_mark` or `rb_gc_mark_movable`), then it will be overwritten with `*ptr = Qundef`. Weak references are implemented using a buffer in `objspace` that stores all the `ptr` in the latest marking phase. After marking has finished, we iterate over the buffer and check if the `*ptr` is a dead object. If it is, then we set `*ptr = Qundef`. Weak references are implemented on the callable method entry (CME) of callcaches, which fixes issue #19436. Weak references are also implemented on `ObjectSpace::WeakMap` and `ObjectSpace::WeakKeyMap`, which have: - Significantly simpler implementations because we no longer need to have multiple tables and do not need to define finalizers on the objects. - Support for compaction because finalizers pin objects and we no longer need to define finalizers on the objects. - Much faster performance (see [benchmarks](#microbenchmarks)). ## Benchmark results ### YJIT-bench We see largely no change in performance or memory usage after this feature. ``` -------------- --------- ---------- --------- ----------- ---------- --------- -------------- ----------- bench base (ms) stddev (%) RSS (MiB) branch (ms) stddev (%) RSS (MiB) branch 1st itr base/branch activerecord 72.3 2.2 51.9 72.9 2.2 51.9 0.99 0.99 chunky-png 889.2 0.3 43.9 874.5 0.3 42.5 1.02 1.02 erubi-rails 21.2 13.5 90.7 21.0 13.3 90.9 1.01 1.01 hexapdf 2557.0 0.8 157.1 2559.2 0.7 197.1 1.01 1.00 liquid-c 65.2 0.4 34.5 65.4 0.4 34.5 0.99 1.00 liquid-compile 62.5 0.4 30.9 62.2 0.4 31.0 1.00 1.01 liquid-render 164.6 0.4 33.1 162.6 0.3 33.1 1.01 1.01 mail 133.3 0.1 46.4 134.4 0.2 46.4 1.03 0.99 psych-load 2066.6 0.2 31.6 2083.6 0.1 31.6 0.99 0.99 railsbench 2027.0 0.5 88.8 2019.4 0.5 89.0 1.01 1.00 ruby-lsp 65.6 3.0 90.1 65.4 3.1 88.5 1.00 1.00 sequel 73.1 1.1 36.6 73.1 1.1 36.6 1.00 1.00 -------------- --------- ---------- --------- ----------- ---------- --------- -------------- ----------- ``` ### Microbenchmarks We can see signficantly improved performance in `ObjectSpace::WeakMap`, with `ObjectSpace::WeakMap#[]=` being nearly 3x faster. Base: ``` ObjectSpace::WeakMap#[]= 1.037M (± 0.5%) i/s - 5.262M in 5.072833s ObjectSpace::WeakMap#[] 12.367M (± 0.9%) i/s - 62.479M in 5.052365s ``` Branch: ``` ObjectSpace::WeakMap#[]= 3.054M (± 0.3%) i/s - 15.448M in 5.058783s ObjectSpace::WeakMap#[] 15.796M (± 4.8%) i/s - 79.245M in 5.028583s ``` Code: ```ruby require "bundler/inline" gemfile do source "https://rubygems.org" gem "benchmark-ips" end wmap = ObjectSpace::WeakMap.new key = Object.new val = Object.new wmap[key] = val Benchmark.ips do |x| x.report("ObjectSpace::WeakMap#[]=") do |times| i = 0 while i < times wmap[Object.new] = Object.new i += 1 end end x.report("ObjectSpace::WeakMap#[]") do |times| i = 0 while i < times wmap[key] wmap[val] # does not exist i += 1 end end end ``` -- https://bugs.ruby-lang.org/

Issue #19783 has been updated by byroot (Jean Boussier). I believe this would fix [Bug #19436] (Call Cache for singleton methods can lead to "memory leaks") ---------------------------------------- Feature #19783: Weak References in the GC https://bugs.ruby-lang.org/issues/19783#change-103972 * Author: peterzhu2118 (Peter Zhu) * Status: Open * Priority: Normal ---------------------------------------- GitHub PR: https://github.com/ruby/ruby/pull/8113 I'm proposing support for weak references in the Ruby garbage collector. This feature adds a new function called `void rb_gc_mark_weak(VALUE *ptr)` which marks `*ptr` as weak, meaning that if no other object strongly marks `*ptr` (using `rb_gc_mark` or `rb_gc_mark_movable`), then it will be overwritten with `*ptr = Qundef`. Weak references are implemented using a buffer in `objspace` that stores all the `ptr` in the latest marking phase. After marking has finished, we iterate over the buffer and check if the `*ptr` is a dead object. If it is, then we set `*ptr = Qundef`. Weak references are implemented on the callable method entry (CME) of callcaches, which fixes issue #19436. Weak references are also implemented on `ObjectSpace::WeakMap` and `ObjectSpace::WeakKeyMap`, which have: - Significantly simpler implementations because we no longer need to have multiple tables and do not need to define finalizers on the objects. - Support for compaction because finalizers pin objects and we no longer need to define finalizers on the objects. - Much faster performance (see [benchmarks](#microbenchmarks)). ## Metrics This patch also adds two metrics, `GC.latest_gc_info(:weak_references_count)` and `GC.latest_gc_info(:retained_weak_references_count)`. These two metrics returns information about the number of weak references registered and the number of weak references retained (references that did not point to a dead object) in the last GC cycle. ## Benchmark results ### YJIT-bench We see largely no change in performance or memory usage after this feature. ``` -------------- --------- ---------- --------- ----------- ---------- --------- -------------- ----------- bench base (ms) stddev (%) RSS (MiB) branch (ms) stddev (%) RSS (MiB) branch 1st itr base/branch activerecord 72.3 2.2 51.9 72.9 2.2 51.9 0.99 0.99 chunky-png 889.2 0.3 43.9 874.5 0.3 42.5 1.02 1.02 erubi-rails 21.2 13.5 90.7 21.0 13.3 90.9 1.01 1.01 hexapdf 2557.0 0.8 157.1 2559.2 0.7 197.1 1.01 1.00 liquid-c 65.2 0.4 34.5 65.4 0.4 34.5 0.99 1.00 liquid-compile 62.5 0.4 30.9 62.2 0.4 31.0 1.00 1.01 liquid-render 164.6 0.4 33.1 162.6 0.3 33.1 1.01 1.01 mail 133.3 0.1 46.4 134.4 0.2 46.4 1.03 0.99 psych-load 2066.6 0.2 31.6 2083.6 0.1 31.6 0.99 0.99 railsbench 2027.0 0.5 88.8 2019.4 0.5 89.0 1.01 1.00 ruby-lsp 65.6 3.0 90.1 65.4 3.1 88.5 1.00 1.00 sequel 73.1 1.1 36.6 73.1 1.1 36.6 1.00 1.00 -------------- --------- ---------- --------- ----------- ---------- --------- -------------- ----------- ``` ### Microbenchmarks We can see signficantly improved performance in `ObjectSpace::WeakMap`, with `ObjectSpace::WeakMap#[]=` being nearly 3x faster. Base: ``` ObjectSpace::WeakMap#[]= 1.037M (± 0.5%) i/s - 5.262M in 5.072833s ObjectSpace::WeakMap#[] 12.367M (± 0.9%) i/s - 62.479M in 5.052365s ``` Branch: ``` ObjectSpace::WeakMap#[]= 3.054M (± 0.3%) i/s - 15.448M in 5.058783s ObjectSpace::WeakMap#[] 15.796M (± 4.8%) i/s - 79.245M in 5.028583s ``` Code: ```ruby require "bundler/inline" gemfile do source "https://rubygems.org" gem "benchmark-ips" end wmap = ObjectSpace::WeakMap.new key = Object.new val = Object.new wmap[key] = val Benchmark.ips do |x| x.report("ObjectSpace::WeakMap#[]=") do |times| i = 0 while i < times wmap[Object.new] = Object.new i += 1 end end x.report("ObjectSpace::WeakMap#[]") do |times| i = 0 while i < times wmap[key] wmap[val] # does not exist i += 1 end end end ``` -- https://bugs.ruby-lang.org/

Issue #19783 has been updated by wks (Kunshan Wang). An alternative to recording the weak reference **fields** during tracing is recording the **objects** that contain weak references on creation. For example, we can record a `imemo_callcache` into a darray when the `imemo_callcache` is created. Then during `gc_update_weak_references`, we update their weak fields. Since there are strictly less objects with weak reference fields than the reference fields themselves, this list should contain less elements. However, the down side is that we need each type that contains weak references to provide a function (or a list of field offsets) that describes how to handle weak fields in a given object. That may require more modification. Some objects may need special treatment for their weak fields. For example, for `WeakKeyMap`, if the object pointed by a key is dead, we need to remove the key-value pair of the `WeakKeyMap` (or even rehashing the map if too many entries are removed) instead of simply setting the key of the entry to nil. Of course it is also possible to do this lazily. Another kind of "special treatment" is calling a call-back (or enqueuing the object, or the associated value in a `WeakKeyMap` to some queue to be processed later) when a weak reference is cleared. This is useful for implementing cleaning-up mechanisms, such as finalizers. If a key of the finalizer table is dead, its values shall be enqueued for execution. This can only be achieved if `gc_update_weak_references` is aware of the objects that contain the weak references instead of the references themselves. ---------------------------------------- Feature #19783: Weak References in the GC https://bugs.ruby-lang.org/issues/19783#change-104101 * Author: peterzhu2118 (Peter Zhu) * Status: Open * Priority: Normal ---------------------------------------- GitHub PR: https://github.com/ruby/ruby/pull/8113 I'm proposing support for weak references in the Ruby garbage collector. This feature adds a new function called `void rb_gc_mark_weak(VALUE *ptr)` which marks `*ptr` as weak, meaning that if no other object strongly marks `*ptr` (using `rb_gc_mark` or `rb_gc_mark_movable`), then it will be overwritten with `*ptr = Qundef`. Weak references are implemented using a buffer in `objspace` that stores all the `ptr` in the latest marking phase. After marking has finished, we iterate over the buffer and check if the `*ptr` is a dead object. If it is, then we set `*ptr = Qundef`. Weak references are implemented on the callable method entry (CME) of callcaches, which fixes issue #19436. Weak references are also implemented on `ObjectSpace::WeakMap` and `ObjectSpace::WeakKeyMap`, which have: - Significantly simpler implementations because we no longer need to have multiple tables and do not need to define finalizers on the objects. - Support for compaction because finalizers pin objects and we no longer need to define finalizers on the objects. - Much faster performance (see [benchmarks](#microbenchmarks)). ## Metrics This patch also adds two metrics, `GC.latest_gc_info(:weak_references_count)` and `GC.latest_gc_info(:retained_weak_references_count)`. These two metrics returns information about the number of weak references registered and the number of weak references retained (references that did not point to a dead object) in the last GC cycle. ## Benchmark results ### YJIT-bench We see largely no change in performance or memory usage after this feature. ``` -------------- --------- ---------- --------- ----------- ---------- --------- -------------- ----------- bench base (ms) stddev (%) RSS (MiB) branch (ms) stddev (%) RSS (MiB) branch 1st itr base/branch activerecord 72.3 2.2 51.9 72.9 2.2 51.9 0.99 0.99 chunky-png 889.2 0.3 43.9 874.5 0.3 42.5 1.02 1.02 erubi-rails 21.2 13.5 90.7 21.0 13.3 90.9 1.01 1.01 hexapdf 2557.0 0.8 157.1 2559.2 0.7 197.1 1.01 1.00 liquid-c 65.2 0.4 34.5 65.4 0.4 34.5 0.99 1.00 liquid-compile 62.5 0.4 30.9 62.2 0.4 31.0 1.00 1.01 liquid-render 164.6 0.4 33.1 162.6 0.3 33.1 1.01 1.01 mail 133.3 0.1 46.4 134.4 0.2 46.4 1.03 0.99 psych-load 2066.6 0.2 31.6 2083.6 0.1 31.6 0.99 0.99 railsbench 2027.0 0.5 88.8 2019.4 0.5 89.0 1.01 1.00 ruby-lsp 65.6 3.0 90.1 65.4 3.1 88.5 1.00 1.00 sequel 73.1 1.1 36.6 73.1 1.1 36.6 1.00 1.00 -------------- --------- ---------- --------- ----------- ---------- --------- -------------- ----------- ``` ### Microbenchmarks We can see signficantly improved performance in `ObjectSpace::WeakMap`, with `ObjectSpace::WeakMap#[]=` being nearly 3x faster. Base: ``` ObjectSpace::WeakMap#[]= 1.037M (± 0.5%) i/s - 5.262M in 5.072833s ObjectSpace::WeakMap#[] 12.367M (± 0.9%) i/s - 62.479M in 5.052365s ``` Branch: ``` ObjectSpace::WeakMap#[]= 3.054M (± 0.3%) i/s - 15.448M in 5.058783s ObjectSpace::WeakMap#[] 15.796M (± 4.8%) i/s - 79.245M in 5.028583s ``` Code: ```ruby require "bundler/inline" gemfile do source "https://rubygems.org" gem "benchmark-ips" end wmap = ObjectSpace::WeakMap.new key = Object.new val = Object.new wmap[key] = val Benchmark.ips do |x| x.report("ObjectSpace::WeakMap#[]=") do |times| i = 0 while i < times wmap[Object.new] = Object.new i += 1 end end x.report("ObjectSpace::WeakMap#[]") do |times| i = 0 while i < times wmap[key] wmap[val] # does not exist i += 1 end end end ``` -- https://bugs.ruby-lang.org/

Issue #19783 has been updated by ko1 (Koichi Sasada). * [Bug #19436] is fixed by checking inline method cache data structures. * So the rest motivation is to support weak reference natively and I don't against about it. * I have two concerns. 1. memory allocation during GC Allocating memory during GC is not good idea in general (because it can be called when memory is not enough). How about to pass the data structure like that? ```C struct weak_ref { VALUE v; struct weak_ref *prev; }; rb_gc_mark_weak(struct weak_ref *ref) { ref->prev = objspace->weaks; objspace->weaks = ref; } mark(){ // do mark all // check weaks struct weak_ref *wref = objspace->weaks; while (wref) { ... } } ``` It doesn't need more allocation while GC. Making such imemo data is also acceptable. 2. huge wrefs I understand it takes proportional time to marking wref counts (the number of `rb_gc_mark_weak()`). I think there is no so much wrefs (especially CME doesn't need it) but it can take a time if there are so many wrefs in an application. Could you make such benchmark? ---------------------------------------- Feature #19783: Weak References in the GC https://bugs.ruby-lang.org/issues/19783#change-104217 * Author: peterzhu2118 (Peter Zhu) * Status: Open * Priority: Normal ---------------------------------------- GitHub PR: https://github.com/ruby/ruby/pull/8113 I'm proposing support for weak references in the Ruby garbage collector. This feature adds a new function called `void rb_gc_mark_weak(VALUE *ptr)` which marks `*ptr` as weak, meaning that if no other object strongly marks `*ptr` (using `rb_gc_mark` or `rb_gc_mark_movable`), then it will be overwritten with `*ptr = Qundef`. Weak references are implemented using a buffer in `objspace` that stores all the `ptr` in the latest marking phase. After marking has finished, we iterate over the buffer and check if the `*ptr` is a dead object. If it is, then we set `*ptr = Qundef`. Weak references are implemented on the callable method entry (CME) of callcaches, which fixes issue #19436. Weak references are also implemented on `ObjectSpace::WeakMap` and `ObjectSpace::WeakKeyMap`, which have: - Significantly simpler implementations because we no longer need to have multiple tables and do not need to define finalizers on the objects. - Support for compaction because finalizers pin objects and we no longer need to define finalizers on the objects. - Much faster performance (see [benchmarks](#microbenchmarks)). ## Metrics This patch also adds two metrics, `GC.latest_gc_info(:weak_references_count)` and `GC.latest_gc_info(:retained_weak_references_count)`. These two metrics returns information about the number of weak references registered and the number of weak references retained (references that did not point to a dead object) in the last GC cycle. ## Benchmark results ### YJIT-bench We see largely no change in performance or memory usage after this feature. ``` -------------- --------- ---------- --------- ----------- ---------- --------- -------------- ----------- bench base (ms) stddev (%) RSS (MiB) branch (ms) stddev (%) RSS (MiB) branch 1st itr base/branch activerecord 72.3 2.2 51.9 72.9 2.2 51.9 0.99 0.99 chunky-png 889.2 0.3 43.9 874.5 0.3 42.5 1.02 1.02 erubi-rails 21.2 13.5 90.7 21.0 13.3 90.9 1.01 1.01 hexapdf 2557.0 0.8 157.1 2559.2 0.7 197.1 1.01 1.00 liquid-c 65.2 0.4 34.5 65.4 0.4 34.5 0.99 1.00 liquid-compile 62.5 0.4 30.9 62.2 0.4 31.0 1.00 1.01 liquid-render 164.6 0.4 33.1 162.6 0.3 33.1 1.01 1.01 mail 133.3 0.1 46.4 134.4 0.2 46.4 1.03 0.99 psych-load 2066.6 0.2 31.6 2083.6 0.1 31.6 0.99 0.99 railsbench 2027.0 0.5 88.8 2019.4 0.5 89.0 1.01 1.00 ruby-lsp 65.6 3.0 90.1 65.4 3.1 88.5 1.00 1.00 sequel 73.1 1.1 36.6 73.1 1.1 36.6 1.00 1.00 -------------- --------- ---------- --------- ----------- ---------- --------- -------------- ----------- ``` ### Microbenchmarks We can see signficantly improved performance in `ObjectSpace::WeakMap`, with `ObjectSpace::WeakMap#[]=` being nearly 3x faster. Base: ``` ObjectSpace::WeakMap#[]= 1.037M (± 0.5%) i/s - 5.262M in 5.072833s ObjectSpace::WeakMap#[] 12.367M (± 0.9%) i/s - 62.479M in 5.052365s ``` Branch: ``` ObjectSpace::WeakMap#[]= 3.054M (± 0.3%) i/s - 15.448M in 5.058783s ObjectSpace::WeakMap#[] 15.796M (± 4.8%) i/s - 79.245M in 5.028583s ``` Code: ```ruby require "bundler/inline" gemfile do source "https://rubygems.org" gem "benchmark-ips" end wmap = ObjectSpace::WeakMap.new key = Object.new val = Object.new wmap[key] = val Benchmark.ips do |x| x.report("ObjectSpace::WeakMap#[]=") do |times| i = 0 while i < times wmap[Object.new] = Object.new i += 1 end end x.report("ObjectSpace::WeakMap#[]") do |times| i = 0 while i < times wmap[key] wmap[val] # does not exist i += 1 end end end ``` -- https://bugs.ruby-lang.org/

Issue #19783 has been updated by peterzhu2118 (Peter Zhu).
[Bug #19436] is fixed by checking inline method cache data structures.
Thank you for fixing the bug. An issue that was brought up to me by the [MMTk team](https://www.mmtk.io/) (the people who are working on implementing alternate GC in Ruby) is that the current implementation of #19436 only works for the mark-sweep garbage collector. This is causing issues for them because some collectors do not mark the whole heap, so we cannot always determine the liveliness of objects. Here are quotes from our discussions:
There are well-known solutions for handling such fields. We re-visit those fields after tracing and clear them. It is important that we either treat them like strong references and trace those fields during tracing, or handle those fields after tracing and clear them if they point to unreachable objects. But in either way, we always have to handle them. We can't just ignore those fields.
How about to pass the data structure like that?
Using a linked list was my original implementation. However, @byroot pointed out that using a linked list is bad for Copy-on-Write performance because these objects are usually long-lived, and so we should avoid writing into them.
Could you make such benchmark?
I will look into benchmarking this. ---------------------------------------- Feature #19783: Weak References in the GC https://bugs.ruby-lang.org/issues/19783#change-104219 * Author: peterzhu2118 (Peter Zhu) * Status: Open * Priority: Normal ---------------------------------------- GitHub PR: https://github.com/ruby/ruby/pull/8113 I'm proposing support for weak references in the Ruby garbage collector. This feature adds a new function called `void rb_gc_mark_weak(VALUE *ptr)` which marks `*ptr` as weak, meaning that if no other object strongly marks `*ptr` (using `rb_gc_mark` or `rb_gc_mark_movable`), then it will be overwritten with `*ptr = Qundef`. Weak references are implemented using a buffer in `objspace` that stores all the `ptr` in the latest marking phase. After marking has finished, we iterate over the buffer and check if the `*ptr` is a dead object. If it is, then we set `*ptr = Qundef`. Weak references are implemented on the callable method entry (CME) of callcaches, which fixes issue #19436. Weak references are also implemented on `ObjectSpace::WeakMap` and `ObjectSpace::WeakKeyMap`, which have: - Significantly simpler implementations because we no longer need to have multiple tables and do not need to define finalizers on the objects. - Support for compaction because finalizers pin objects and we no longer need to define finalizers on the objects. - Much faster performance (see [benchmarks](#microbenchmarks)). ## Metrics This patch also adds two metrics, `GC.latest_gc_info(:weak_references_count)` and `GC.latest_gc_info(:retained_weak_references_count)`. These two metrics returns information about the number of weak references registered and the number of weak references retained (references that did not point to a dead object) in the last GC cycle. ## Benchmark results ### YJIT-bench We see largely no change in performance or memory usage after this feature. ``` -------------- --------- ---------- --------- ----------- ---------- --------- -------------- ----------- bench base (ms) stddev (%) RSS (MiB) branch (ms) stddev (%) RSS (MiB) branch 1st itr base/branch activerecord 72.3 2.2 51.9 72.9 2.2 51.9 0.99 0.99 chunky-png 889.2 0.3 43.9 874.5 0.3 42.5 1.02 1.02 erubi-rails 21.2 13.5 90.7 21.0 13.3 90.9 1.01 1.01 hexapdf 2557.0 0.8 157.1 2559.2 0.7 197.1 1.01 1.00 liquid-c 65.2 0.4 34.5 65.4 0.4 34.5 0.99 1.00 liquid-compile 62.5 0.4 30.9 62.2 0.4 31.0 1.00 1.01 liquid-render 164.6 0.4 33.1 162.6 0.3 33.1 1.01 1.01 mail 133.3 0.1 46.4 134.4 0.2 46.4 1.03 0.99 psych-load 2066.6 0.2 31.6 2083.6 0.1 31.6 0.99 0.99 railsbench 2027.0 0.5 88.8 2019.4 0.5 89.0 1.01 1.00 ruby-lsp 65.6 3.0 90.1 65.4 3.1 88.5 1.00 1.00 sequel 73.1 1.1 36.6 73.1 1.1 36.6 1.00 1.00 -------------- --------- ---------- --------- ----------- ---------- --------- -------------- ----------- ``` ### Microbenchmarks We can see signficantly improved performance in `ObjectSpace::WeakMap`, with `ObjectSpace::WeakMap#[]=` being nearly 3x faster. Base: ``` ObjectSpace::WeakMap#[]= 1.037M (± 0.5%) i/s - 5.262M in 5.072833s ObjectSpace::WeakMap#[] 12.367M (± 0.9%) i/s - 62.479M in 5.052365s ``` Branch: ``` ObjectSpace::WeakMap#[]= 3.054M (± 0.3%) i/s - 15.448M in 5.058783s ObjectSpace::WeakMap#[] 15.796M (± 4.8%) i/s - 79.245M in 5.028583s ``` Code: ```ruby require "bundler/inline" gemfile do source "https://rubygems.org" gem "benchmark-ips" end wmap = ObjectSpace::WeakMap.new key = Object.new val = Object.new wmap[key] = val Benchmark.ips do |x| x.report("ObjectSpace::WeakMap#[]=") do |times| i = 0 while i < times wmap[Object.new] = Object.new i += 1 end end x.report("ObjectSpace::WeakMap#[]") do |times| i = 0 while i < times wmap[key] wmap[val] # does not exist i += 1 end end end ``` -- https://bugs.ruby-lang.org/

Issue #19783 has been updated by peterzhu2118 (Peter Zhu). I ran the benchmark you wrote in #19436. On my machine it looks like this branch is a little bit slower: Branch: ``` ruby 3.3.0dev (2023-08-18T19:42:54Z weak-ref-gc 21d00dd558) [arm64-darwin22] 10.663139 4.448306 15.111445 ( 15.157204) ``` Master: ``` ruby 3.3.0dev (2023-08-18T14:25:36Z master c8d6419985) [arm64-darwin22] 10.670126 4.158576 14.828702 ( 14.839812) ``` Benchmark: ```ruby require "benchmark" puts RUBY_DESCRIPTION puts(Benchmark.measure do 100_000.times { |i| str = "x" * 1_000_000 def str.foo = nil eval "def call#{i}(s) = s.foo" send "call#{i}", str } end) ``` ---------------------------------------- Feature #19783: Weak References in the GC https://bugs.ruby-lang.org/issues/19783#change-104220 * Author: peterzhu2118 (Peter Zhu) * Status: Open * Priority: Normal ---------------------------------------- GitHub PR: https://github.com/ruby/ruby/pull/8113 I'm proposing support for weak references in the Ruby garbage collector. This feature adds a new function called `void rb_gc_mark_weak(VALUE *ptr)` which marks `*ptr` as weak, meaning that if no other object strongly marks `*ptr` (using `rb_gc_mark` or `rb_gc_mark_movable`), then it will be overwritten with `*ptr = Qundef`. Weak references are implemented using a buffer in `objspace` that stores all the `ptr` in the latest marking phase. After marking has finished, we iterate over the buffer and check if the `*ptr` is a dead object. If it is, then we set `*ptr = Qundef`. Weak references are implemented on the callable method entry (CME) of callcaches, which fixes issue #19436. Weak references are also implemented on `ObjectSpace::WeakMap` and `ObjectSpace::WeakKeyMap`, which have: - Significantly simpler implementations because we no longer need to have multiple tables and do not need to define finalizers on the objects. - Support for compaction because finalizers pin objects and we no longer need to define finalizers on the objects. - Much faster performance (see [benchmarks](#microbenchmarks)). ## Metrics This patch also adds two metrics, `GC.latest_gc_info(:weak_references_count)` and `GC.latest_gc_info(:retained_weak_references_count)`. These two metrics returns information about the number of weak references registered and the number of weak references retained (references that did not point to a dead object) in the last GC cycle. ## Benchmark results ### YJIT-bench We see largely no change in performance or memory usage after this feature. ``` -------------- --------- ---------- --------- ----------- ---------- --------- -------------- ----------- bench base (ms) stddev (%) RSS (MiB) branch (ms) stddev (%) RSS (MiB) branch 1st itr base/branch activerecord 72.3 2.2 51.9 72.9 2.2 51.9 0.99 0.99 chunky-png 889.2 0.3 43.9 874.5 0.3 42.5 1.02 1.02 erubi-rails 21.2 13.5 90.7 21.0 13.3 90.9 1.01 1.01 hexapdf 2557.0 0.8 157.1 2559.2 0.7 197.1 1.01 1.00 liquid-c 65.2 0.4 34.5 65.4 0.4 34.5 0.99 1.00 liquid-compile 62.5 0.4 30.9 62.2 0.4 31.0 1.00 1.01 liquid-render 164.6 0.4 33.1 162.6 0.3 33.1 1.01 1.01 mail 133.3 0.1 46.4 134.4 0.2 46.4 1.03 0.99 psych-load 2066.6 0.2 31.6 2083.6 0.1 31.6 0.99 0.99 railsbench 2027.0 0.5 88.8 2019.4 0.5 89.0 1.01 1.00 ruby-lsp 65.6 3.0 90.1 65.4 3.1 88.5 1.00 1.00 sequel 73.1 1.1 36.6 73.1 1.1 36.6 1.00 1.00 -------------- --------- ---------- --------- ----------- ---------- --------- -------------- ----------- ``` ### Microbenchmarks We can see signficantly improved performance in `ObjectSpace::WeakMap`, with `ObjectSpace::WeakMap#[]=` being nearly 3x faster. Base: ``` ObjectSpace::WeakMap#[]= 1.037M (± 0.5%) i/s - 5.262M in 5.072833s ObjectSpace::WeakMap#[] 12.367M (± 0.9%) i/s - 62.479M in 5.052365s ``` Branch: ``` ObjectSpace::WeakMap#[]= 3.054M (± 0.3%) i/s - 15.448M in 5.058783s ObjectSpace::WeakMap#[] 15.796M (± 4.8%) i/s - 79.245M in 5.028583s ``` Code: ```ruby require "bundler/inline" gemfile do source "https://rubygems.org" gem "benchmark-ips" end wmap = ObjectSpace::WeakMap.new key = Object.new val = Object.new wmap[key] = val Benchmark.ips do |x| x.report("ObjectSpace::WeakMap#[]=") do |times| i = 0 while i < times wmap[Object.new] = Object.new i += 1 end end x.report("ObjectSpace::WeakMap#[]") do |times| i = 0 while i < times wmap[key] wmap[val] # does not exist i += 1 end end end ``` -- https://bugs.ruby-lang.org/
participants (4)
-
byroot (Jean Boussier)
-
ko1 (Koichi Sasada)
-
peterzhu2118 (Peter Zhu)
-
wks (Kunshan Wang)