Project

General

Profile

Actions

Feature #19783

closed

Weak References in the GC

Added by peterzhu2118 (Peter Zhu) over 1 year ago. Updated about 1 year ago.

Status:
Closed
Assignee:
-
Target version:
-
[ruby-core:114274]

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

Related issues 2 (0 open2 closed)

Related to Ruby master - Bug #19436: Call Cache for singleton methods can lead to "memory leaks"Closedko1 (Koichi Sasada)Actions
Related to Ruby master - Bug #19863: ruby 3.3.0dev rarely gets `[BUG] Segmentation fault`ClosedActions
Actions #1

Updated by peterzhu2118 (Peter Zhu) over 1 year ago

  • Description updated (diff)
Actions #2

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) over 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.

    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?

      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?

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)
Actions #8

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

Actions #9

Updated by byroot (Jean Boussier) about 1 year ago

  • Related to Bug #19863: ruby 3.3.0dev rarely gets `[BUG] Segmentation fault` added
Actions

Also available in: Atom PDF

Like4
Like0Like0Like0Like0Like0Like0Like0Like0Like0