The id2ref table could contain dead entries which should not be passed
into rb_gc_location. Also, we already update references in gc_update_references
using rb_gc_vm_weak_table_foreach so we do not need to update it again.
This makes `RBobject` `4B` larger on 32 bit systems
but simplifies the implementation a lot.
[Feature #21353]
Co-authored-by: Jean Boussier <byroot@ruby-lang.org>
Superclasses can't be modified by user code, so do not need namespace
indirection. For example Object.superclass is always BasicObject, no
matter what modules are included onto it.
Classes from the default namespace are not writable, however they do not
transition to too_complex until they have been written to inside a user
namespace. So this assertion is invalid (as is the previous location it
was) but it doesn't seem to provide us much value.
Co-authored-by: Aaron Patterson <tenderlove@ruby-lang.org>
When classes are booted, they should all be writeable unless namespaces
are enabled. This commit adds an assertion to ensure that classes are
writable.
We don't need the key, so we can improve performance by only iterating
on the value.
This will also fix the MMTk build because looking up the key in
rb_id_table_foreach requires locking the VM, which is not supported in
the MMTk worker threads.
Building that table will likely malloc several time which
can trigger GC and cause race condition by freeing objects
that were just added to the table.
Disabling GC to prevent the race condition isn't elegant,
but iven this is a deprecated callpath that is executed at
most once per process, it seems acceptable.
If the object isn't shareable and already has a object_id
we can access it without a lock.
If we need to generate an ID, we may need to lock to find
the child shape.
We also generate the next `object_id` using atomics.
Given classes and modules have a different set of fields in every
namespace, we can't store the object_id in fields for them.
Given that some space was freed in `RClass` we can store it there
instead.
The intial complex shape implementation never allowed objects
other than T_OBJECT to become too complex, unless we run out of
shapes.
I don't see any reason to prevent that.
Ref: https://github.com/ruby/ruby/pull/6931
The macro RCLASS_EXT() accesses the prime classext directly, but it can be
valid only in a limited situation when namespace is enabled.
So, to prevent using RCLASS_EXT() in the wrong way, rename the macro and
let the developer check it is ok to access the prime classext or not.
As well as `RB_OBJ_SHAPE_ID` -> `rb_obj_shape_id`
and `RSHAPE` is now a simple alias for `rb_shape_lookup`.
I tried to turn all these into `static inline` but I'm having
trouble with `RUBY_EXTERN rb_shape_tree_t *rb_shape_tree_ptr;`
not being exposed as I'd expect.
[Feature #15408]
Matz decided to deprecate it for Ruby 2.6 or 2.7 but that never
actually happened.
Given the object_id table is a contention point for Ractors
it seems sensible to finally deprecate this API so we can
generate and store object ids more efficiently in the future.
Use `st_foreach_with_replace` rather than to call `st_insert`
from inside `st_foreach`, this saves from having to disable GC.
Co-Authored-By: Peter Zhu <peter@peterzhu.ca>
And get rid of the `obj_to_id_tbl`
It's no longer needed, the `object_id` is now stored inline
in the object alongside instance variables.
We still need the inverse table in case `_id2ref` is invoked, but
we lazily build it by walking the heap if that happens.
The `object_id` concern is also no longer a GC implementation
concern, but a generic implementation.
Co-Authored-By: Matt Valentine-House <matt@eightbitraptor.com>
Ivars will longer be the only thing stored inline
via shapes, so keeping the `iv_index` and `ivptr` names
would be confusing.
Instance variables won't be the only thing stored inline
via shapes, so keeping the `ivptr` name would be confusing.
`field` encompass anything that can be stored in a VALUE array.
Similarly, `gen_ivtbl` becomes `gen_fields_tbl`.
This halves the amount of memory used for embedded RTypedData if they
are one VALUE (8 bytes on 64-bit platforms) over the slot size limit.
For Set, on 64-bit it uses an embedded 56-byte struct. With the
previous implementation, the embedded structs starts at offset 32,
resulting in a total size of 88. Since that is over the 80 byte
limit, it goes to the next highest bucket, 160 bytes, wasting 72
bytes. This allows it to fit in a 80 byte bucket, which reduces
the total size for small sets of from 224 bytes (160 bytes
embedded, 64 bytes malloc, 72 bytes wasted in embedding) to 144
bytes (80 bytes embedded, 64 bytes malloc, 0 bytes wasted in
embedding).
Any other embedded RTypedData will see similar advantages if they
are currently one VALUE over the limit.
To implement this, remove the typed_flag from struct RTypedData.
Embed the typed_flag information in the type member, which is
now a tagged pointer using VALUE type, using the bottom low 2 bits
as flags (1 bit for typed flag, the other for the embedded flag).
To get the actual pointer, RTYPEDDATA_TYPE masks out
the low 2 bits and then casts. That moves the RTypedData data
pointer from offset 32 to offset 24 (on 64-bit).
Vast amount of code in the internals (and probably external C
extensions) expects the following code to work for both RData and
non-embedded RTypedData:
```c
DATA_PTR(obj) = some_pointer;
```
Allow this to work by moving the data pointer in RData between
the dmark and dfree pointers, so it is at the same offset (24
on 64-bit).
Other than these changes to the include files, the only changes
needed were to gc.c, to account for the new struct layouts,
handle setting the low bits in the type member, and to use
RTYPEDDATA_TYPE(obj) instead of RTYPEDDATA(obj)->type.
Similar to 4a040eeb0d880b67a5005cce382122fd5b629b99, I noticed the test
for FL_FINALIZE checking FL_ABLE in a profile, and we shouldn't need to
do that here.
This implements a hash set which is wait-free for lookup and lock-free
for insert (unless resizing) to use for fstring de-duplication.
As highlighted in https://bugs.ruby-lang.org/issues/19288, heavy use of
fstrings (frozen interned strings) can significantly reduce the
parallelism of Ractors.
I tried a few other approaches first: using an RWLock, striping a series
of RWlocks (partitioning the hash N-ways to reduce lock contention), and
putting a cache in front of it. All of these improved the situation, but
were unsatisfying as all still required locks for writes (and granular
locks are awkward, since we run the risk of needing to reach a vm
barrier) and this table is somewhat write-heavy.
My main reference for this was Cliff Click's talk on a lock free
hash-table for java https://www.youtube.com/watch?v=HJ-719EGIts. It
turns out this lock-free hash set is made easier to implement by a few
properties:
* We only need a hash set rather than a hash table (we only need keys,
not values), and so the full entry can be written as a single VALUE
* As a set we only need lookup/insert/delete, no update
* Delete is only run inside GC so does not need to be atomic (It could
be made concurrent)
* I use rb_vm_barrier for the (rare) table rebuilds (It could be made
concurrent) We VM lock (but don't require other threads to stop) for
table rebuilds, as those are rare
* The conservative garbage collector makes deferred replication easy,
using a T_DATA object
Another benefits of having a table specific to fstrings is that we
compare by value on lookup/insert, but by identity on delete, as we only
want to remove the exact string which is being freed. This is faster and
provides a second way to avoid the race condition in
https://bugs.ruby-lang.org/issues/21172.
This is a pretty standard open-addressing hash table with quadratic
probing. Similar to our existing st_table or id_table. Deletes (which
happen on GC) replace existing keys with a tombstone, which is the only
type of update which can occur. Tombstones are only cleared out on
resize.
Unlike st_table, the VALUEs are stored in the hash table itself
(st_table's bins) rather than as a compact index. This avoids an extra
pointer dereference and is possible because we don't need to preserve
insertion order. The table targets a load factor of 2 (it is enlarged
once it is half full).
This inverse table is only useful if `ObjectSpace._id2ref` is used,
which is extremely rare. The only notable exception is the `drb` gem
and even then it has an option not to rely on `_id2ref`.
So if we assume this table will never be looked up, we can just
not maintain it, and if it turns out `_id2ref` is called, we
can lock the VM and re-build it.
```
compare-ruby: ruby 3.5.0dev (2025-04-10T09:44:40Z master 684cfa42d7) +YJIT +PRISM [arm64-darwin24]
built-ruby: ruby 3.5.0dev (2025-04-10T10:13:43Z lazy-id-to-obj d3aa9626cc) +YJIT +PRISM [arm64-darwin24]
warming up..
| |compare-ruby|built-ruby|
|:----------|-----------:|---------:|
|baseline | 26.364M| 25.974M|
| | 1.01x| -|
|object_id | 10.293M| 14.202M|
| | -| 1.38x|
```
Using `rb_obj_clone` introduce other problems, such as `initialize_*`
callbacks invocation in the context of the parent ractor.
So we can revert back to copy the content of the object slots,
but in a way that is aware of size pools.