| Age | Commit message (Collapse) | Author |
|
https://github.com/ruby/mmtk/commit/e5e2c1c347
|
|
https://github.com/ruby/mmtk/commit/87290e45b2
|
|
https://github.com/ruby/mmtk/commit/59d27203e2
|
|
https://github.com/ruby/mmtk/commit/e34d5cf32f
|
|
https://github.com/ruby/mmtk/commit/350625ebb3
|
|
https://github.com/ruby/mmtk/commit/12a3904b04
|
|
https://github.com/ruby/mmtk/commit/8813e76bf8
|
|
Redos commit 544770d which seems to have accidentally been undone in b27d935.
|
|
|
|
|
|
https://github.com/ruby/mmtk/commit/308936296a
|
|
This allows the mutator thread to dump its backtrace when a GC thread crashes.
https://github.com/ruby/mmtk/commit/40ff9ffee7
|
|
https://github.com/ruby/mmtk/commit/c3e338bb25
|
|
https://github.com/ruby/mmtk/commit/58210c88ed
|
|
https://github.com/ruby/mmtk/commit/42adba630e
|
|
The argument to `is_data_encoding` is assumed to be `T_DATA`.
|
|
This makes it easier to visualize in profilers which one is non-parallel.
https://github.com/ruby/mmtk/commit/ba68b2ef3b
|
|
This commit allows objects that are safe to be freed in parallel to do so.
A decrease in object freeing time can be seen in profiles.
The benchmarks don't show much difference.
Before:
-------------- -------------------- ---------- ---------
bench sequential free (ms) stddev (%) RSS (MiB)
activerecord 242.3 7.4 84.3
chunky-png 439.1 0.6 75.6
erubi-rails 1221.2 4.2 132.7
hexapdf 1544.8 1.8 429.1
liquid-c 42.7 7.4 48.5
liquid-compile 41.4 8.3 52.2
liquid-render 100.6 3.0 56.8
mail 108.9 2.1 65.1
psych-load 1536.9 0.6 43.4
railsbench 1633.5 2.6 146.2
rubocop 126.5 15.8 142.1
ruby-lsp 129.6 9.7 112.2
sequel 47.9 6.5 44.6
shipit 1152.0 2.7 315.2
-------------- -------------------- ---------- ---------
After:
-------------- ------------------ ---------- ---------
bench parallel free (ms) stddev (%) RSS (MiB)
activerecord 235.1 5.5 87.4
chunky-png 440.8 0.8 68.1
erubi-rails 1105.3 0.8 128.0
hexapdf 1578.3 4.1 405.1
liquid-c 42.6 7.1 48.4
liquid-compile 41.5 8.1 52.1
liquid-render 101.2 2.8 53.3
mail 109.7 2.7 64.8
psych-load 1567.7 1.1 44.4
railsbench 1644.9 1.9 150.9
rubocop 125.6 15.4 148.5
ruby-lsp 127.9 5.8 104.6
sequel 48.2 6.1 44.1
shipit 1215.3 4.7 320.5
-------------- ------------------ ---------- ---------
https://github.com/ruby/mmtk/commit/4f0b5fd2eb
|
|
https://github.com/ruby/mmtk/commit/290a2aec4e
|
|
|
|
Fixes issue pointed out in https://bugs.ruby-lang.org/issues/21084#note-7.
The following script crashes:
wmap = ObjectSpace::WeakMap.new
GC.disable # only manual GCs
GC.start
GC.start
retain = []
50.times do
k = Object.new
wmap[k] = true
retain << k
end
GC.start # wmap promoted, other objects still young
retain.clear
GC.start(full_mark: false)
wmap.keys.each(&:itself) # call method on keys to cause crash
|
|
This commit implements moving Immix in MMTk, which allows objects to move
in the GC.
The performance of this implementation is not yet amazing. It is very
similar to non-moving Immix in many of them and slightly slower in others.
The benchmark results is shown below.
-------------- ----------------- ---------- ---------
bench Moving Immix (ms) stddev (%) RSS (MiB)
activerecord 241.9 0.5 86.6
chunky-png 447.8 0.8 74.9
erubi-rails 1183.9 0.8 136.1
hexapdf 1607.9 2.6 402.3
liquid-c 45.4 6.7 44.9
liquid-compile 44.1 9.3 53.0
liquid-render 105.4 4.5 55.9
lobsters 650.1 9.7 418.4
mail 115.4 2.1 64.4
psych-load 1656.8 0.8 43.6
railsbench 1653.5 1.3 149.8
rubocop 127.0 15.6 142.1
ruby-lsp 130.7 10.5 99.4
sequel 52.8 7.2 45.6
shipit 1187.0 3.9 311.0
-------------- ----------------- ---------- ---------
-------------- --------------------- ---------- ---------
bench Non-moving Immix (ms) stddev (%) RSS (MiB)
activerecord 218.9 2.7 86.1
chunky-png 464.6 0.8 66.7
erubi-rails 1119.0 4.3 132.7
hexapdf 1539.8 1.8 425.2
liquid-c 40.6 6.9 45.2
liquid-compile 40.6 8.1 52.9
liquid-render 99.3 2.3 48.3
mail 107.4 5.3 65.4
psych-load 1535.6 1.0 39.5
railsbench 1565.6 1.1 149.6
rubocop 122.5 14.3 146.7
ruby-lsp 128.4 10.7 106.4
sequel 44.1 4.0 45.7
shipit 1154.5 2.7 358.5
-------------- --------------------- ---------- ---------
|
|
|
|
|
|
https://github.com/ruby/mmtk/commit/f4c46cabc7
|
|
|
|
[Feature #21084]
# Summary
The current way of marking weak references uses `rb_gc_mark_weak(VALUE *ptr)`.
This presents challenges because Ruby's GC is incremental, meaning that if the
`ptr` changes (e.g. realloc'd or free'd), then we could have an invalid memory
access. This also overwrites `*ptr = Qundef` if `*ptr` is dead, which prevents
any cleanup to be run (e.g. freeing memory or deleting entries from hash
tables). This ticket proposes `rb_gc_declare_weak_references` which declares
that an object has weak references and calls a cleanup function after marking,
allowing the object to clean up any memory for dead objects.
# Introduction
In [[Feature #19783]](https://bugs.ruby-lang.org/issues/19783), I introduced an
API allowing objects to mark weak references, the function signature looks like
this:
```c
void rb_gc_mark_weak(VALUE *ptr);
```
`rb_gc_mark_weak` is called during the marking phase of the GC to specify that
the memory at `ptr` holds a pointer to a Ruby object that is weakly referenced.
`rb_gc_mark_weak` appends this pointer to a list that is processed after the
marking phase of the GC. If the object at `*ptr` is no longer alive, then it
overwrites the object reference with a special value (`*ptr = Qundef`).
However, this API resulted in two challenges:
1. Ruby's default GC is incremental, which means that the GC is not ran in one
phase, but rather split into chunks of work that interleaves with Ruby
execution. The `ptr` passed into `rb_gc_mark_weak` could be on the malloc
heap, and that memory could be realloc'd or even free'd. We had to use
workarounds such as `rb_gc_remove_weak` to ensure that there were no illegal
memory accesses. This made `rb_gc_mark_weak` difficult to use, impacted
runtime performance, and increased memory usage.
2. When an object dies, `rb_gc_mark_weak` only overwites the reference with
`Qundef`. This means that if we want to do any cleanup (e.g. free a piece of
memory or delete a hash table entry), we could not do that and had to defer
this process elsewhere (e.g. during marking or runtime).
In this ticket, I'm proposing a new API for weak references. Instead of an
object marking its weak references during the marking phase, the object declares
that it has weak references using the `rb_gc_declare_weak_references` function.
This declaration occurs during runtime (e.g. after the object has been created)
rather than during GC.
After an object declares that it has weak references, it will have its callback
function called after marking as long as that object is alive. This callback
function can then call a special function `rb_gc_handle_weak_references_alive_p`
to determine whether its references are alive. This will allow the callback
function to do whatever it wants on the object, allowing it to perform any
cleanup work it needs.
This significantly simplifies the code for `ObjectSpace::WeakMap` and
`ObjectSpace::WeakKeyMap` because it no longer needs to have the workarounds for
the limitations of `rb_gc_mark_weak`.
# Performance
The performance results below demonstrate that `ObjectSpace::WeakMap#[]=` is now
about 60% faster because the implementation has been simplified and the number
of allocations has been reduced. We can see that there is not a significant
impact on the performance of `ObjectSpace::WeakMap#[]`.
Base:
```
ObjectSpace::WeakMap#[]=
4.620M (± 6.4%) i/s (216.44 ns/i) - 23.342M in 5.072149s
ObjectSpace::WeakMap#[]
30.967M (± 1.9%) i/s (32.29 ns/i) - 154.998M in 5.007157s
```
Branch:
```
ObjectSpace::WeakMap#[]=
7.336M (± 2.8%) i/s (136.31 ns/i) - 36.755M in 5.013983s
ObjectSpace::WeakMap#[]
30.902M (± 5.4%) i/s (32.36 ns/i) - 155.901M in 5.064060s
```
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
```
# Alternative designs
Currently, `rb_gc_declare_weak_references` is designed to be an internal-only
API. This allows us to assume the object types that call
`rb_gc_declare_weak_references`. In the future, if we want to open up this API
to third parties, we may want to change this function to something like:
```c
void rb_gc_add_cleaner(VALUE obj, void (*callback)(VALUE obj));
```
This will allow the third party to implement a custom `callback` that gets
called after the marking phase of GC to clean up any dead references. I chose
not to implement this design because it is less efficient as we would need to
store a mapping from `obj` to `callback`, which requires extra memory.
|
|
|
|
This heap emulates the growth characteristics of the Ruby default GC's
heap. By default, the heap grows by 40%, requires at least 20% empty
after a GC, and allows at most 65% empty before it shrinks the heap. This
is all configurable via the same environment variables the default GC
uses (`RUBY_GC_HEAP_FREE_SLOTS_GOAL_RATIO`, `RUBY_GC_HEAP_FREE_SLOTS_MIN_RATIO`,
`RUBY_GC_HEAP_FREE_SLOTS_MAX_RATIO`, respectively).
The Ruby heap can be enabled via the `MMTK_HEAP_MODE=ruby` environment
variable.
Compared to the dynamic heap in MMTk (which uses the MemBalancer algorithm),
the Ruby heap allows the heap to grow more generously, which uses a bit
more memory but offers significant performance gains because it runs GC
much less frequently.
We can see in the benchmarks below that this Ruby heap heap gives faster
performance than the dynamic heap in every benchmark, with over 2x faster
in many of them. We see that memory is often around 10-20% higher with
certain outliers that use significantly more memory like hexapdf and
erubi-rails. We can also see that this brings MMTk's Ruby heap much
closer in performance to the default GC.
Ruby heap benchmark results:
-------------- -------------- ---------- ---------
bench ruby heap (ms) stddev (%) RSS (MiB)
activerecord 233.6 10.7 85.9
chunky-png 457.1 1.1 79.3
erubi-rails 1148.0 3.8 133.3
hexapdf 1570.5 2.4 403.0
liquid-c 42.8 5.3 43.4
liquid-compile 41.3 7.6 52.6
liquid-render 102.8 3.8 55.3
lobsters 651.9 8.0 426.3
mail 106.4 1.8 67.2
psych-load 1552.1 0.8 43.4
railsbench 1707.2 6.0 145.6
rubocop 127.2 15.3 148.8
ruby-lsp 136.6 11.7 113.7
sequel 47.2 5.9 44.4
shipit 1197.5 3.6 301.0
-------------- -------------- ---------- ---------
Dynamic heap benchmark results:
-------------- ----------------- ---------- ---------
bench dynamic heap (ms) stddev (%) RSS (MiB)
activerecord 845.3 3.1 76.7
chunky-png 525.9 0.4 38.9
erubi-rails 2694.9 3.4 115.8
hexapdf 2344.8 5.6 164.9
liquid-c 73.7 5.0 40.5
liquid-compile 107.1 6.8 40.3
liquid-render 147.2 1.7 39.5
lobsters 697.6 4.5 342.0
mail 224.6 2.1 64.0
psych-load 4326.7 0.6 37.4
railsbench 3218.0 5.5 124.7
rubocop 203.6 6.1 110.9
ruby-lsp 350.7 3.2 79.0
sequel 121.8 2.5 39.6
shipit 1510.1 3.1 220.8
-------------- ----------------- ---------- ---------
Default GC benchmark results:
-------------- --------------- ---------- ---------
bench default GC (ms) stddev (%) RSS (MiB)
activerecord 148.4 0.6 67.9
chunky-png 440.2 0.7 57.0
erubi-rails 722.7 0.3 97.8
hexapdf 1466.2 1.7 254.3
liquid-c 32.5 3.6 42.3
liquid-compile 31.2 1.9 35.4
liquid-render 88.3 0.7 30.8
lobsters 633.6 7.0 305.4
mail 76.6 1.6 53.2
psych-load 1166.2 1.3 29.1
railsbench 1262.9 2.3 114.7
rubocop 105.6 0.8 95.4
ruby-lsp 101.6 1.4 75.4
sequel 27.4 1.2 33.1
shipit 1083.1 1.5 163.4
-------------- --------------- ---------- ---------
https://github.com/ruby/mmtk/commit/c0ca29922d
|
|
https://github.com/ruby/mmtk/commit/c4cca6c1c3
|
|
Adding a fast path for bump pointer allocator can improve allocation
performance.
For the following microbenchmark with MMTK_HEAP_MIN=100MiB:
10_000_000.times { String.new }
Before:
810.7 ms ± 8.3 ms [User: 790.9 ms, System: 40.3 ms]
After:
777.9 ms ± 10.4 ms [User: 759.0 ms, System: 37.9 ms]
https://github.com/ruby/mmtk/commit/0ff5c9f579
|
|
https://github.com/ruby/mmtk/commit/2185189df4
|
|
This will allow the Ruby backtrace, memory mapping, etc. to be outputted
when a Ruby mutator thread panics.
https://github.com/ruby/mmtk/commit/d10fd325dd
|
|
https://github.com/ruby/mmtk/commit/ed9036c295
|
|
https://github.com/ruby/mmtk/commit/4e789e118b
|
|
not hold
|
|
This guard was removed in https://github.com/ruby/ruby/pull/13497
on the justification that some GC may need to be notified even for
immediate.
But the two currently available GCs don't, and there are plenty
of assumtions GCs don't everywhere, notably in YJIT and ZJIT.
This optimization is also not so micro (but not huge either).
I routinely see 1-2% wasted there on micro-benchmarks.
So perhaps if in the future we actually need this, it might make
sense to introduce a way for GCs to declare that as an option,
but in the meantime it's extra overhead with little gain.
|
|
I believe this was accidentally left in as part of
2beb3798bac52624c3170138f8ef65869f1da6c0
|
|
This reverts commit 228d13f6ed914d1e7f6bd2416e3f5be8283be865.
This commit makes default.c and mmtk.c depend on shape.h, which prevents
them from building independently.
|
|
|
|
|
|
Attempt to fix the following SEGV:
```
ruby(gc_mark) ../src/gc/default/default.c:4429
ruby(gc_mark_children+0x45) [0x560b380bf8b5] ../src/gc/default/default.c:4625
ruby(gc_mark_stacked_objects) ../src/gc/default/default.c:4647
ruby(gc_mark_stacked_objects_all) ../src/gc/default/default.c:4685
ruby(gc_marks_rest) ../src/gc/default/default.c:5707
ruby(gc_marks+0x4e7) [0x560b380c41c1] ../src/gc/default/default.c:5821
ruby(gc_start) ../src/gc/default/default.c:6502
ruby(heap_prepare+0xa4) [0x560b380c4efc] ../src/gc/default/default.c:2074
ruby(heap_next_free_page) ../src/gc/default/default.c:2289
ruby(newobj_cache_miss) ../src/gc/default/default.c:2396
ruby(RB_SPECIAL_CONST_P+0x0) [0x560b380c5df4] ../src/gc/default/default.c:2420
ruby(RB_BUILTIN_TYPE) ../src/include/ruby/internal/value_type.h:184
ruby(newobj_init) ../src/gc/default/default.c:2136
ruby(rb_gc_impl_new_obj) ../src/gc/default/default.c:2500
ruby(newobj_of) ../src/gc.c:996
ruby(rb_imemo_new+0x37) [0x560b380d8bed] ../src/imemo.c:46
ruby(imemo_fields_new) ../src/imemo.c:105
ruby(rb_imemo_fields_new) ../src/imemo.c:120
```
I have no reproduction, but my understanding based on the backtrace
and error is that GC is triggered inside `newobj_init` causing the
new object to be marked while in a incomplete state.
I believe the fix is to pass the `shape_id` down to `newobj_init`
so it can be set before the GC has a chance to trigger.
|
|
This should be less common than than many of the other flags, so should
not inflate the heap too much. This is desirable because reducing the
number of remembered objects will improve minor GC speeds.
|
|
Fix sign-compare warning
|
|
|
|
|
|
This allows us to do less work when allocating a fresh page.
|
|
|
|
Since we do not run a Ractor barrier before forking, it's possible that
another other Ractor is halfway through allocating an object during forking.
This may lead to allocated_objects_count being off by one.
For example, the following script reproduces the bug:
100.times do |i|
Ractor.new(i) do |j|
10000.times do |i|
"#{j}-#{i}"
end
Ractor.receive
end
pid = fork { GC.verify_internal_consistency }
_, status = Process.waitpid2 pid
raise unless status.success?
end
We need to run with `taskset -c 1` to force it to use a single CPU core
to more consistenly reproduce the bug:
heap_pages_final_slots: 1, total_freed_objects: 16628
test.rb:8: [BUG] inconsistent live slot number: expect 19589, but 19588.
ruby 4.0.0dev (2025-11-25T03:06:55Z master 55892f5994) +PRISM [x86_64-linux]
-- Control frame information -----------------------------------------------
c:0007 p:---- s:0029 e:000028 l:y b:---- CFUNC :verify_internal_consistency
c:0006 p:0004 s:0025 e:000024 l:n b:---- BLOCK test.rb:8 [FINISH]
c:0005 p:---- s:0022 e:000021 l:y b:---- CFUNC :fork
c:0004 p:0012 s:0018 E:0014c0 l:n b:---- BLOCK test.rb:8
c:0003 p:0024 s:0011 e:000010 l:y b:0001 METHOD <internal:numeric>:257
c:0002 p:0005 s:0006 E:001730 l:n b:---- EVAL test.rb:1 [FINISH]
c:0001 p:0000 s:0003 E:001d20 l:y b:---- DUMMY [FINISH]
-- Ruby level backtrace information ----------------------------------------
test.rb:1:in '<main>'
<internal:numeric>:257:in 'times'
test.rb:8:in 'block in <main>'
test.rb:8:in 'fork'
test.rb:8:in 'block (2 levels) in <main>'
test.rb:8:in 'verify_internal_consistency'
-- Threading information ---------------------------------------------------
Total ractor count: 1
Ruby thread count for this ractor: 1
-- C level backtrace information -------------------------------------------
ruby(rb_print_backtrace+0x14) [0x61b67ac48b60] vm_dump.c:1105
ruby(rb_vm_bugreport) vm_dump.c:1450
ruby(rb_bug_without_die_internal+0x5f) [0x61b67a818a28] error.c:1098
ruby(rb_bug) error.c:1116
ruby(gc_verify_internal_consistency_+0xbdd) [0x61b67a83d8ed] gc/default/default.c:5186
ruby(gc_verify_internal_consistency+0x2d) [0x61b67a83d960] gc/default/default.c:5241
ruby(rb_gc_verify_internal_consistency) gc/default/default.c:8950
ruby(gc_verify_internal_consistency_m) gc/default/default.c:8966
ruby(vm_call_cfunc_with_frame_+0x10d) [0x61b67a9e50fd] vm_insnhelper.c:3902
ruby(vm_sendish+0x111) [0x61b67a9eeaf1] vm_insnhelper.c:6124
ruby(vm_exec_core+0x84) [0x61b67aa07434] insns.def:903
ruby(vm_exec_loop+0xa) [0x61b67a9f8155] vm.c:2811
ruby(rb_vm_exec) vm.c:2787
ruby(vm_yield_with_cref+0x90) [0x61b67a9fd2ea] vm.c:1865
ruby(vm_yield) vm.c:1873
ruby(rb_yield) vm_eval.c:1362
ruby(rb_protect+0xef) [0x61b67a81fe6f] eval.c:1154
ruby(rb_f_fork+0x16) [0x61b67a8e98ab] process.c:4293
ruby(rb_f_fork) process.c:4284
|
|
|