This commit adds `jit_calls` and `jit_threshold_reached` to iseq dumps which
dumps the number of calls to the iseq and whether the iseq has been JIT
compiled or not, respectively.
After the rlgc merge, this method and also ObjectSpace.shareable_objects_from_roots could
return foreign unshareables which violates Ractor containment.
Zombie objspaces are created a Ractor is joined but `#value` wasn't
called on it. They are also created when a Ractor is collected and
didn't have its objspace absorbed by another Ractor. If we do a heap
dump of all objects, we want to see the shareables in those objspaces.
Adjust bootstraptest and test-all for the per-Ractor local GC, and add
regression tests for the Ractor lifecycle and cross-objspace cases.
Co-authored-by: Claude <noreply@anthropic.com>
I benchmarked various values of STR_DUPLICATE_MAX_EMBED_LEN and the
performance impacts. For String#dup, creating a view is faster until it
is below 256 bytes. Unsurprisingly, if we add modification to the string,
then it is faster to eagerly create a copy than use a view. Surprisingly,
if we append to the string, it's still faster to go from embedded -> extended
than shared -> extended. However, both of these cases will incur a memory
penalty as the remainder of the slot is wasted memory.
Benchmark:
require "benchmark"
TIMES = 10_000_000
Benchmark.bm do |x|
[64, 128, 256, 512, 1024].each do |len|
len -= 25 # Subtract header length
x.report("String#dup for #{len.to_s}") do
pid = fork do
str = ("a" * len).freeze
i = 0
while i < TIMES
str.dup
i += 1
end
end
Process.wait(pid)
end
x.report("String#dup + String#[]= for #{len.to_s}") do
pid = fork do
str = ("a" * len).freeze
i = 0
while i < TIMES
dup = str.dup
dup[0] = "1"
i += 1
end
end
Process.wait(pid)
end
x.report("String#dup + String#<< for #{len.to_s}") do
pid = fork do
str = ("a" * len).freeze
i = 0
while i < TIMES
dup = str.dup
dup << "1"
i += 1
end
end
Process.wait(pid)
end
end
end
Result:
1024
user system total real
String#dup for 39 0.000085 0.000646 0.207714 ( 0.208041)
String#dup + String#[]= for 39 0.000067 0.000379 0.499957 ( 0.500514)
String#dup + String#<< for 39 0.000069 0.000413 0.600205 ( 0.600984)
String#dup for 103 0.000068 0.000374 0.236938 ( 0.237424)
String#dup + String#[]= for 103 0.000063 0.000401 0.501791 ( 0.502506)
String#dup + String#<< for 103 0.000066 0.000395 0.600041 ( 0.600870)
String#dup for 231 0.000066 0.000370 0.281924 ( 0.282409)
String#dup + String#[]= for 231 0.000064 0.000355 0.574779 ( 0.575599)
String#dup + String#<< for 231 0.000070 0.000395 0.691011 ( 0.691972)
String#dup for 487 0.000070 0.000366 0.355631 ( 0.356230)
String#dup + String#[]= for 487 0.000067 0.000391 0.653774 ( 0.654509)
String#dup + String#<< for 487 0.000066 0.000359 0.891413 ( 0.892607)
String#dup for 999 0.000068 0.000394 0.527350 ( 0.528033)
String#dup + String#[]= for 999 0.000067 0.000378 0.786794 ( 0.787682)
String#dup + String#<< for 999 0.000063 0.000363 1.070604 ( 1.071885)
512
user system total real
String#dup for 39 0.000083 0.000639 0.207305 ( 0.207687)
String#dup + String#[]= for 39 0.000067 0.000398 0.501968 ( 0.502645)
String#dup + String#<< for 39 0.000071 0.000414 0.586533 ( 0.587365)
String#dup for 103 0.000064 0.000397 0.222104 ( 0.222600)
String#dup + String#[]= for 103 0.000069 0.000389 0.483612 ( 0.484240)
String#dup + String#<< for 103 0.000069 0.000380 0.606659 ( 0.607510)
String#dup for 231 0.000067 0.000388 0.292563 ( 0.293070)
String#dup + String#[]= for 231 0.000064 0.000389 0.573774 ( 0.574479)
String#dup + String#<< for 231 0.000069 0.000413 0.739569 ( 0.740614)
String#dup for 487 0.000067 0.000419 0.370516 ( 0.371072)
String#dup + String#[]= for 487 0.000060 0.000337 0.676507 ( 0.677282)
String#dup + String#<< for 487 0.000074 0.000410 0.911670 ( 0.916480)
String#dup for 999 0.000069 0.000397 0.239633 ( 0.240049)
String#dup + String#[]= for 999 0.000066 0.000370 1.196397 ( 1.198272)
String#dup + String#<< for 999 0.000072 0.000396 1.683562 ( 1.686244)
256
user system total real
String#dup for 39 0.000073 0.000588 0.198103 ( 0.198485)
String#dup + String#[]= for 39 0.000067 0.000397 0.475345 ( 0.476011)
String#dup + String#<< for 39 0.000074 0.000408 0.589001 ( 0.589778)
String#dup for 103 0.000073 0.000420 0.244381 ( 0.244818)
String#dup + String#[]= for 103 0.000065 0.000421 0.480720 ( 0.481569)
String#dup + String#<< for 103 0.000070 0.000414 0.589164 ( 0.590123)
String#dup for 231 0.000077 0.000447 0.272169 ( 0.272640)
String#dup + String#[]= for 231 0.000067 0.000425 0.517280 ( 0.518037)
String#dup + String#<< for 231 0.000064 0.000409 0.681593 ( 0.682572)
String#dup for 487 0.000065 0.000390 0.229708 ( 0.230153)
String#dup + String#[]= for 487 0.000067 0.000395 0.872966 ( 0.874233)
String#dup + String#<< for 487 0.000071 0.000411 1.317758 ( 1.320085)
String#dup for 999 0.000070 0.000397 0.255197 ( 0.255609)
String#dup + String#[]= for 999 0.000068 0.000380 1.174781 ( 1.176700)
String#dup + String#<< for 999 0.000067 0.000389 1.729608 ( 1.738797)
128
user system total real
String#dup for 39 0.000105 0.000950 0.205384 ( 0.206565)
String#dup + String#[]= for 39 0.000066 0.000406 0.483924 ( 0.485308)
String#dup + String#<< for 39 0.000075 0.000433 0.590150 ( 0.591641)
String#dup for 103 0.000076 0.000414 0.225900 ( 0.227975)
String#dup + String#[]= for 103 0.000098 0.000604 0.499234 ( 0.503926)
String#dup + String#<< for 103 0.000070 0.000400 0.619562 ( 0.627411)
String#dup for 231 0.000073 0.000445 0.241433 ( 0.243606)
String#dup + String#[]= for 231 0.000067 0.000402 0.843665 ( 0.853293)
String#dup + String#<< for 231 0.000071 0.000392 0.990903 ( 0.994555)
String#dup for 487 0.000073 0.000403 0.234920 ( 0.236132)
String#dup + String#[]= for 487 0.000077 0.000445 0.944582 ( 0.956001)
String#dup + String#<< for 487 0.000069 0.000410 1.337099 ( 1.341250)
String#dup for 999 0.000072 0.000420 0.233520 ( 0.234182)
String#dup + String#[]= for 999 0.000072 0.000419 1.186288 ( 1.188972)
String#dup + String#<< for 999 0.000069 0.000436 1.685112 ( 1.689536)
We should cap the max length for embedding duplicated strings because it
could be costly in performance and memory usage for very large embedded
strings. Instead, we can use a view into the parent string and perform
copy-on-write.
Instead of spilling into a raw buffer, `RObject` now spills
into an `IMEMO/fields` object like other types.
From an instance variable layout standpoint, an extended `RObject`
is now identical to a `RTypedData`, as in they both store the
reference to their `IMEMO/fields` at the same offset (`VALUE * 2`).
One positive consequence of this is that the only case where a `T_OBJECT`
needs sweeping is if a finalizer was registered.
YJIT now side exit when it need to write a new ivar into a `RObject`
that is out of space. This is unlikely to cause a performance regression
as this codepath isn't supposed to happen after warmup given `max_iv_count`
is recorded on classes, so future objects should be large enough.
Hence it's best not to waste executable memory for such codepath.
ZJIT lost support for writting into extended `RObject`.
Most of the code to support it is there it's just missing an
implementation of `RBASIC_SET_SHAPE_ID`, which recently changed
to strip some bits out of the shape (see comments).
I will leave it to the ZJIT team to implement it (sorry).
There are a few future cleanups planned that I keep for followups:
- We can now get rid of the `ROBJECT_HEAP` flag, it's redundant with
the shape layout bits.
- `imemo_fields` can now embed the `st_table` when complex.
Co-Authored-By: John Hawthorn <john@hawthorn.email>
Co-authored-by: Randy Stauner <randy@r4s6.net>
These guards no longer match current mswin behavior. The affected tests
pass reliably on an x64-mswin64_140 build, repeatedly and under -j16 for
the flaky-history ones.
Only the mswin conditions are removed. test_ractor and test_transcode
keep their darwin and s390x guards. test_thread_queue keeps the MinGW
omit and drops only the retired AppVeyor branch. test_box re-enables
box_1 only, since box_2 still crashes at process exit under mswin and
stays pended.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
File.readlines slurps each Ractor's whole ~8MB dump into memory at once.
Because dump_all dumps the shared heap, the concurrent Ractors' readlines
buffers feed back into each other's dumps, so peak memory grows
superlinearly and the stop-the-world barrier thrashes past the 100s
timeout on macOS/M:N. File.foreach keeps peak memory flat (NR=16:
2.96GB -> 47MB) while still parsing every line.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Object tracing listens to the NEWOBJ hook to see all objects allocated
while it is active. Previously it also enabled a FREEOBJ tracepoint to
drop each object's record as the object was freed.
However the FREEOBJ tracepoint only fires while tracing is active. An
object recorded during tracing can be freed after tracing stops, and
that free was missed, leaving a stale record in object_table keyed by a
freed object. These dangling keys are unsafe during compaction, which
was previously mitigated with rb_gc_pointer_to_heap_p (which I'd like to
stop using). These dangling keys could also become incorrectly
associated with a new object allocated in its place.
Instead we can declare object_table's keys as weak references. The GC
then invokes our weak reference callback on every collection, whether or
not tracing is running, letting us reap the records of objects about to
be freed. This callback runs before the FREEOBJ hook, which makes the
FREEOBJ tracepoint unnecessary.
This adds "free_immediately: false" to the heap dump for any T_DATA
which doesn't specify RUBY_TYPED_FREE_IMMEDIATELY. These are the objects
which end up as T_ZOMBIE and will have their dfree callback deferred to
later when the program is running holding the GVL.
This is useful to include because there is more overhead to freeing
these objects, and it's worth knowing how many there are.
to show C level backtrace for the following failure:
```
1) Error:
TestObjSpaceRactor#test_undefine_finalizer:
Test::Unit::ProxyError: execution of Test::Unit::CoreAssertions#assert_separately expired timeout (10 sec)
pid 861177 killed by SIGTERM (signal 15)
|
/tmp/ruby/src/trunk-yjit/test/objspace/test_ractor.rb:17:in 'TestObjSpaceRactor#test_undefine_finalizer'
```
If another ractor is calling for GC, we need to prevent the current one
from joining the barrier. Otherwise, our half-built object will be marked.
The repro script was:
test.rb:
```ruby
require "objspace"
1000.times do
ObjectSpace.trace_object_allocations do
r = Ractor.new do
_obj = 'a' * 1024
end
r.join
end
end
```
$ untilfail lldb -b ./exe/ruby -o "target create ./exe/ruby" -o "run test.rb" -o continue
It would fail at `ractor_port_mark`, rp->r was a garbage value. Credit to John for finding the
solution.
Co-authored-by: John Hawthorn <john.hawthorn@shopify.com>
* Added `Ractor::Port`
* `Ractor::Port#receive` (support multi-threads)
* `Rcator::Port#close`
* `Ractor::Port#closed?`
* Added some methods
* `Ractor#join`
* `Ractor#value`
* `Ractor#monitor`
* `Ractor#unmonitor`
* Removed some methods
* `Ractor#take`
* `Ractor.yield`
* Change the spec
* `Racotr.select`
You can wait for multiple sequences of messages with `Ractor::Port`.
```ruby
ports = 3.times.map{ Ractor::Port.new }
ports.map.with_index do |port, ri|
Ractor.new port,ri do |port, ri|
3.times{|i| port << "r#{ri}-#{i}"}
end
end
p ports.each{|port| pp 3.times.map{port.receive}}
```
In this example, we use 3 ports, and 3 Ractors send messages to them respectively.
We can receive a series of messages from each port.
You can use `Ractor#value` to get the last value of a Ractor's block:
```ruby
result = Ractor.new do
heavy_task()
end.value
```
You can wait for the termination of a Ractor with `Ractor#join` like this:
```ruby
Ractor.new do
some_task()
end.join
```
`#value` and `#join` are similar to `Thread#value` and `Thread#join`.
To implement `#join`, `Ractor#monitor` (and `Ractor#unmonitor`) is introduced.
This commit changes `Ractor.select()` method.
It now only accepts ports or Ractors, and returns when a port receives a message or a Ractor terminates.
We removes `Ractor.yield` and `Ractor#take` because:
* `Ractor::Port` supports most of similar use cases in a simpler manner.
* Removing them significantly simplifies the code.
We also change the internal thread scheduler code (thread_pthread.c):
* During barrier synchronization, we keep the `ractor_sched` lock to avoid deadlocks.
This lock is released by `rb_ractor_sched_barrier_end()`
which is called at the end of operations that require the barrier.
* fix potential deadlock issues by checking interrupts just before setting UBF.
https://bugs.ruby-lang.org/issues/21262
This commit inlines instructions for Class#new. To make this work, we
added a new YARV instructions, `opt_new`. `opt_new` checks whether or
not the `new` method is the default allocator method. If it is, it
allocates the object, and pushes the instance on the stack. If not, the
instruction jumps to the "slow path" method call instructions.
Old instructions:
```
> ruby --dump=insns -e'Object.new'
== disasm: #<ISeq:<main>@-e:1 (1,0)-(1,10)>
0000 opt_getconstant_path <ic:0 Object> ( 1)[Li]
0002 opt_send_without_block <calldata!mid:new, argc:0, ARGS_SIMPLE>
0004 leave
```
New instructions:
```
> ./miniruby --dump=insns -e'Object.new'
== disasm: #<ISeq:<main>@-e:1 (1,0)-(1,10)>
0000 opt_getconstant_path <ic:0 Object> ( 1)[Li]
0002 putnil
0003 swap
0004 opt_new <calldata!mid:new, argc:0, ARGS_SIMPLE>, 11
0007 opt_send_without_block <calldata!mid:initialize, argc:0, FCALL|ARGS_SIMPLE>
0009 jump 14
0011 opt_send_without_block <calldata!mid:new, argc:0, ARGS_SIMPLE>
0013 swap
0014 pop
0015 leave
```
This commit speeds up basic object allocation (`Foo.new`) by 60%, but
classes that take keyword parameters see an even bigger benefit because
no hash is allocated when instantiating the object (3x to 6x faster).
Here is an example that uses `Hash.new(capacity: 0)`:
```
> hyperfine "ruby --disable-gems -e'i = 0; while i < 10_000_000; Hash.new(capacity: 0); i += 1; end'" "./ruby --disable-gems -e'i = 0; while i < 10_000_000; Hash.new(capacity: 0); i += 1; end'"
Benchmark 1: ruby --disable-gems -e'i = 0; while i < 10_000_000; Hash.new(capacity: 0); i += 1; end'
Time (mean ± σ): 1.082 s ± 0.004 s [User: 1.074 s, System: 0.008 s]
Range (min … max): 1.076 s … 1.088 s 10 runs
Benchmark 2: ./ruby --disable-gems -e'i = 0; while i < 10_000_000; Hash.new(capacity: 0); i += 1; end'
Time (mean ± σ): 627.9 ms ± 3.5 ms [User: 622.7 ms, System: 4.8 ms]
Range (min … max): 622.7 ms … 633.2 ms 10 runs
Summary
./ruby --disable-gems -e'i = 0; while i < 10_000_000; Hash.new(capacity: 0); i += 1; end' ran
1.72 ± 0.01 times faster than ruby --disable-gems -e'i = 0; while i < 10_000_000; Hash.new(capacity: 0); i += 1; end'
```
This commit changes the backtrace for `initialize`:
```
aaron@tc ~/g/ruby (inline-new)> cat test.rb
class Foo
def initialize
puts caller
end
end
def hello
Foo.new
end
hello
aaron@tc ~/g/ruby (inline-new)> ruby -v test.rb
ruby 3.4.2 (2025-02-15 revision d2930f8e7a) +PRISM [arm64-darwin24]
test.rb:8:in 'Class#new'
test.rb:8:in 'Object#hello'
test.rb:11:in '<main>'
aaron@tc ~/g/ruby (inline-new)> ./miniruby -v test.rb
ruby 3.5.0dev (2025-03-28T23:59:40Z inline-new c4157884e4) +PRISM [arm64-darwin24]
test.rb:8:in 'Object#hello'
test.rb:11:in '<main>'
```
It also increases memory usage for calls to `new` by 122 bytes:
```
aaron@tc ~/g/ruby (inline-new)> cat test.rb
require "objspace"
class Foo
def initialize
puts caller
end
end
def hello
Foo.new
end
puts ObjectSpace.memsize_of(RubyVM::InstructionSequence.of(method(:hello)))
aaron@tc ~/g/ruby (inline-new)> make runruby
RUBY_ON_BUG='gdb -x ./.gdbinit -p' ./miniruby -I./lib -I. -I.ext/common ./tool/runruby.rb --extout=.ext -- --disable-gems ./test.rb
656
aaron@tc ~/g/ruby (inline-new)> ruby -v test.rb
ruby 3.4.2 (2025-02-15 revision d2930f8e7a) +PRISM [arm64-darwin24]
544
```
Thanks to @ko1 for coming up with this idea!
Co-Authored-By: John Hawthorn <john@hawthorn.email>
Given that the currently planned ractor local GC implementation
performance will heavilly be influenced by the number of shareable
objects it would be valuable to be able to know how many of them
are in the heap.
This test occasionally fail because it runs into a String instance
that had its `==` method removed.
I couldn't identify where this String comes from, but in general
when using `each_object` it's best to not assume returned objectd
are functional.
By just inverting the operands of `==` we ensure it's always
`String#==` that is called.
```
1) Error:
TestObjSpace#test_memsize_of_root_shared_string:
NoMethodError: undefined method '==' for #<String:0x00007f9b50e8c978>
/tmp/ruby/src/trunk-random1/test/objspace/test_objspace.rb:35:in 'block in TestObjSpace#test_memsize_of_root_shared_string'
/tmp/ruby/src/trunk-random1/test/objspace/test_objspace.rb:35:in 'ObjectSpace.each_object'
/tmp/ruby/src/trunk-random1/test/objspace/test_objspace.rb:35:in 'TestObjSpace#test_memsize_of_root_shared_string'
```
When reference updating ObjectSpace.trace_object_allocations, we need to
check whether the object is valid or not because it does not mark the
object so the object may be dead. This can cause a segmentation fault
if the object is on a free heap page.
For example, the following script crashes:
require "objspace"
objs = []
ObjectSpace.trace_object_allocations do
1_000_000.times do
objs << Object.new
end
end
objs = nil
# Free pages that the objs were on
GC.start
# Run compaction and check that it doesn't crash
GC.compact
We need to reinsert into the ST table when an object moves because it is
a numtable that hashes on the object address, so when an object moves we
need to reinsert it rather than just updating the key.
[Bug #20892]
Until the introduction of that method, it was impossible for a
Module name not to be valid JSON, hence it wasn't going through
the slower escaping function.
This assumption no longer hold.
This commit splits gc.c into two files:
- gc.c now only contains code not specific to Ruby GC. This includes
code to mark objects (which the GC implementation may choose not to
use) and wrappers for internal APIs that the implementation may need
to use (e.g. locking the VM).
- gc_impl.c now contains the implementation of Ruby's GC. This includes
marking, sweeping, compaction, and statistics. Most importantly,
gc_impl.c only uses public APIs in Ruby and a limited set of functions
exposed in gc.c. This allows us to build gc_impl.c independently of
Ruby and plug Ruby's GC into itself.
The test assumes `:foo` is a static symbol, but that is only true
if a literal `:foo` was parsed before `"foo".to_sym` was evaled:
```ruby
require 'objspace'
foo_sym = "foo".to_sym
puts ObjectSpace.dump(eval(":foo"))
```
```
{"address":"0x100fb46d0", "type":"SYMBOL", "shape_id":10, "slot_size":40, "class":"0x100d3e9c8", "frozen":true, "bytesize":3, "value":"foo", "memsize":40, "flags":{"wb_protected":true, "marking":true, "marked":true}}
```