Feature #19783
closedWeak References in the GC
Description
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).
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:
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
Updated by byroot (Jean Boussier) over 1 year ago
- Related to Bug #19436: Call Cache for singleton methods can lead to "memory leaks" added
Updated by byroot (Jean Boussier) over 1 year ago
I believe this would fix [Bug #19436] (Call Cache for singleton methods can lead to "memory leaks")
Updated by wks (Kunshan Wang) about 1 year ago
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.
Updated by ko1 (Koichi Sasada) about 1 year ago
-
[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.
-
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?
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. -
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?
-
Updated by peterzhu2118 (Peter Zhu) about 1 year ago
[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 (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 (Jean Boussier) 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.
Updated by peterzhu2118 (Peter Zhu) about 1 year ago
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:
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)
Updated by peterzhu2118 (Peter Zhu) about 1 year ago
- Status changed from Open to Closed
Applied in changeset git|bfb395c620b811b4b3cb7d535d58721268af285d.
Implement weak references in the GC
[Feature #19783]
This commit adds support for weak references in the GC through the
function rb_gc_mark_weak
. Unlike strong references, weak references
does not mark the object, but rather lets the GC know that an object
refers to another one. If the child object is freed, the pointer from
the parent object is overwritten with Qundef
.
Co-Authored-By: Jean Boussier byroot@ruby-lang.org
Updated by byroot (Jean Boussier) about 1 year ago
- Related to Bug #19863: ruby 3.3.0dev rarely gets `[BUG] Segmentation fault` added