197 Commits
Author SHA1 Message Date
John Hawthorn 73055acd57 Forbid setting ivars on Ractor shareable objects
Previously we had a check that objects which were shareable, but not
frozen on a non-main Ractor would raise an exception if instance
variables were read. This prevented a non-main Ractor reading
non-shareable objects.

This changes the behaviour to forbid all writes to shareable objects
(even when unfrozen). This is done by re-purposing the frozen flag on
the object shape to reflect (FL_FROZEN || FL_SHAREABLE).

With IV writes forbidden on all shareable objects whether or not they
are frozen (with the exception of classes and modules), we can also
remove the check against reading instance variables, since no unsafe
instance variables can be set.

[Bug #22291]
2026-09-16 12:26:11 -07:00
Nobuyoshi Nakada b51ae934f7 [Feature #22215] Remove vm_core dependency from gc header
Keep `internal/gc.h` usable without VM-private definitions so bundled
extensions do not inherit `vm_core.h` dependencies.
2026-08-07 09:59:15 +09:00
Peter Zhu 3760fedbff Handle shape ID capacity overflow
If we allocate objects larger than 1024 bytes, the capacity in the shape
ID will overflow. This commit fixes it set the capacity to SHAPE_ID_CAPACITY_MAX
in that case and falls back to rb_gc_obj_slot_size instead.
2026-07-21 11:00:57 -04:00
Jean BoussierandJohn Hawthorn 40f105bd55 Change RStruct to match SHAPE_ID_LAYOUT_EXTENDED
THe downside is that we now always reserve one 8B reference for the
IMEMO/fields, even for structs that never have any ivars.

But it is an acceptable tradeoff given that ivars on structs aren't
rare.

Co-Authored-By: John Hawthorn <john@hawthorn.email>
2026-07-21 08:30:12 +02:00
Jean Boussier 74bd057f9c Get rid of ROBJECT_FIELDS_HASH 2026-07-15 20:52:47 +02:00
Jean Boussier 0afd762b9e Get rid of ROBJECT_FIELDS_CAPACITY
It's barely used.
2026-07-15 20:52:47 +02:00
Jean Boussier 0b681246d6 Get rid of ROBJECT_FIELDS_COUNT
It's basrely used.
2026-07-15 20:52:47 +02:00
Jean Boussier efb3a73445 rb_imemo_fields_ptr: assume embedded 2026-07-15 20:52:47 +02:00
Jean Boussier 86951165b8 Get rid of ROBJECT_HEAP flag
This flag is now entirely redundant with the shape_id layout bits.
2026-07-15 15:43:28 +02:00
Jean Boussier 365865067d Move rb_obj_using_gen_fields_table_p into gc.c
Break dependency of `shape.h` on `internal/struct.h`
2026-07-15 10:57:43 +02:00
Jean Boussier 4bd94ccbbd variable.c: only filter out object_id, not all internal ivars 2026-07-14 23:50:17 +02:00
Jean Boussier 24337e3cbe Refactor rb_obj_copy_ivar
Share more code with `rb_copy_generic_ivar`.
2026-07-14 23:50:17 +02:00
Jean Boussier 71ee81f7d9 imemo.c: embed the st_table inside rb_fields
Followup: https://github.com/ruby/ruby/pull/17631

Now that complex `RObject` always have an IMEMO/fields, we no
longer need to have an external `st_table` to replicate the
memory layout of `RObject`.
2026-07-14 23:50:17 +02:00
e0083cffa3 Refactor RObject to use T_IMEMO/fields_obj on overflow
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>
2026-07-14 10:44:39 +02:00
Jean Boussier 3d350d6670 Use rb_obj_shape_slot_size over rb_gc_obj_slot_size
`rb_gc_obj_slot_size` has to check the page metadata to
get the slot size, which likely cause cache misses.

`rb_obj_shape_slot_size` does simple arithmetic using
only a few shape bits.

```
compare-ruby: ruby 4.1.0dev (2026-07-08T03:48:16Z master 01c5b2e890) +PRISM [arm64-darwin25]
built-ruby: ruby 4.1.0dev (2026-07-08T10:17:41Z gc-changed-slot-size 08029b0f7c) +PRISM [arm64-darwin25]
warming up.
```

|                        |compare-ruby|built-ruby|
|:-----------------------|-----------:|---------:|
|binary_concat_embedded  |      5.262M|    5.707M|
|                        |           -|     1.08x|
2026-07-08 13:39:59 +02:00
Jean Boussier 64e9855098 Introduce rb_shape_transition_slot_size 2026-07-07 13:12:35 +02:00
Jean Boussier f9d0531cc7 shape.h: get rid of rb_shape_id_with_capacity 2026-07-07 13:12:35 +02:00
Jean Boussier 263f06ad49 RBASIC_SET_SHAPE_ID: also strip the layout bits
Like the capacity part, the layout part of an object shape
almost never changes. The few exceptions are:

  - On allocation.
  - On being compacted by GC.
  - When RObject oberflows.

As such it simplifies a lot of code if `RBASIC_SET_SHAPE_ID` strips
the layout bits, as we often copy the shape from IMEMO/fields to
the owner object and vice-versa.

Also change `RBASIC_SET_SHAPE_ID_WITH_CAPACITY` into
`RBASIC_SET_FULL_SHAPE_ID` so it can be used for assigning both
capacity and layout.
2026-07-07 13:12:35 +02:00
Jean Boussier 588d5910d5 shape.h: rb_shape_capacity -> rb_shape_embedded_capacity
To avoid confusion with the actual capacity which may be larger
if the object has been extended (ROBJECT_HEAP).
2026-07-07 13:12:35 +02:00
Peter Zhu db7a9a1d6d Dynamically determine max shape capacity
SHAPE_MAX_CAPACITY needed to be below or equal to the maximum capacity
allocatable by the GC otherwise it would crash. This commit decouples that
by adding back max_capacity in the rb_shape_tree_t and adding a new GC
API function rb_gc_impl_max_allocation_size to report the maximum size
allocatable by the GC.
2026-07-06 20:51:54 -04:00
Peter Zhu 870c8d6a50 Use shape bits to store capacity of object
This commit uses 7 shape bits to store the capacity of the object instead
of the heap ID. This allows for slot sizes with up to capacity of 127.
This removes the abstraction leak of slot sizes from the GC, which allows
more flexibility in the GC.

This implementation should currently make no difference for the default
GC as it will end up creating the same root shapes. We can see that there
is basically no change in benchmarks:

--------------  ------------  ------------  ------------  ------------  --------------  -------------  -----------------
bench            master (ms)     RSS (MiB)   branch (ms)     RSS (MiB)  branch 1st itr  master/branch  RSS master/branch
activerecord     99.6 ± 3.9%   75.5 ± 0.3%   98.9 ± 2.4%   74.0 ± 1.3%           0.919          1.007              1.021
chunky-png      328.2 ± 0.6%   83.0 ± 3.9%  336.4 ± 0.7%   87.0 ± 2.2%           0.979          0.976              0.954
erubi-rails     440.4 ± 1.7%  138.1 ± 0.3%  431.0 ± 0.5%  135.2 ± 0.0%           0.992          1.022              1.022
hexapdf         855.6 ± 0.7%  577.0 ± 0.1%  856.7 ± 0.9%  649.0 ± 2.6%           0.993          0.999              0.889
liquid-c         21.6 ± 8.2%  69.6 ± 14.7%   23.2 ± 8.4%  64.6 ± 15.4%           0.993          0.930              1.077
liquid-compile   19.9 ± 7.9%   47.0 ± 5.7%   20.2 ± 8.7%   46.9 ± 5.1%           0.925          0.987              1.001
liquid-render    54.0 ± 2.7%   55.3 ± 8.6%   55.2 ± 2.7%   55.9 ± 9.0%           0.977          0.978              0.990
lobsters        351.6 ± 0.7%  336.8 ± 0.2%  357.0 ± 0.6%  335.4 ± 0.1%           0.997          0.985              1.004
mail             47.5 ± 3.1%   75.6 ± 2.7%   46.8 ± 1.3%   72.3 ± 0.5%           0.974          1.016              1.045
psych-load      829.1 ± 1.2%   55.0 ± 0.4%  832.9 ± 0.5%   55.3 ± 0.7%           1.006          0.995              0.995
railsbench      713.8 ± 0.4%  134.6 ± 1.1%  717.9 ± 1.0%  138.2 ± 1.2%           0.992          0.994              0.974
rubocop          73.1 ± 6.0%  106.7 ± 1.7%   73.2 ± 6.1%  106.4 ± 1.4%           0.997          0.998              1.003
ruby-lsp         69.0 ± 1.2%   78.3 ± 0.2%   67.8 ± 2.3%   74.2 ± 0.0%           1.043          1.017              1.055
sequel           21.7 ± 6.8%   56.1 ± 1.6%   21.0 ± 4.4%   55.9 ± 1.1%           0.873          1.032              1.003
shipit          608.9 ± 1.4%  160.2 ± 0.5%  619.2 ± 1.4%  157.3 ± 0.2%           1.012          0.983              1.018
--------------  ------------  ------------  ------------  ------------  --------------  -------------  -----------------
2026-07-03 21:14:12 -04:00
Peter Zhu f8b25fe249 Remove dead rb_shape_get_next_iv_shape 2026-06-29 20:47:29 -04:00
Aaron Patterson c4d09f416c Reapply "Reserve 2 bits for expressing object layout (#17139)" (#17158)
This reverts commit ddb5055d96.
2026-06-01 19:30:58 +00:00
Aaron Patterson ddb5055d96 Revert "Reserve 2 bits for expressing object layout (#17139)"
This reverts commit 63d9f090b5.
2026-05-29 16:41:31 -07:00
63d9f090b5 Reserve 2 bits for expressing object layout (#17139)
* Reserve 2 bits for expressing object layout

We would like to make instance variable reads in the JIT compiler faster
(as well as simplify the JIT implementation).  Currently, in order to
read an instance variable, we have to:

1. Test for heap object
2. Load object to a 64 bit register
3. Mask the object header
4. Bit test against the masked header
5. JNE
6. Load field

We would like to:

1. Test for heap object
2. Load object shape to a 32 bit register
3. Bit test against the shape
4. JNE
5. Load field

The way we fetch instance variables is not consistent across objects.
In order to realize our goal, we need to encode object layout inside the
shape.  If we encode object layout inside the shape, then the shape
itself will guarantee that the access pattern generated by the JIT
compiler is correct.

We should encode the following load patterns into the shape tag bits.
This way we can share shapes on transitions, but be able to
differentiate the access patterns for the JIT compiler.  In other words,
two objects can have an `@a -> @b -> @c` transition and share the same
shape, but the tag bits can differentiate the access pattern so that the
JIT compiler can be confident that the machine code is correct.

Here are the patterns:

1. Embedded/Extended T_OBJECT Instance Variables

Objects with direct references to instance variables or via malloc
buffer

2. Objects with fields_objects fields

These are Data and TypedData objects.  They have an associated axillary
imemo/fields object that stores the instance variables.  The access
pattern is `object[2] + 2`.  The fields object is the 3rd field, and the
instance variables start at +2 inside the fields object.  The fields
object itself is a Ruby object, so it contains the usual header bits +
class headers.

3. Non Boxable Classes / Modules

This is similar to Objects with fields_objects, but the fields object is
stored at a different offset.  We’re differentiating this from boxable
classes and modules because those are harder to support.

4. Other

"Other" pattern is for objects that are rare, or have
difficult-to-implement access patterns.  This includes:

* Boxable classes and modules
* Structs (for now)
* Objects that use the geniv table

Proposed shape bit layout:

```
  Current shape_id_t is 32 bits:
  31        28 27 26 25 24 23 22        19 18                         0
  +-----------+--+--+--+--+--+------------+----------------------------+
  | unused    |L1|L0|OI|FR|CX| heap index | shape tree offset          |
  +-----------+--+--+--+--+--+------------+----------------------------+
               |  |  |  |  |  |            |
               |  |  |  |  |  |            +-- bits 0-18: SHAPE_ID_OFFSET_MASK
               |  |  |  |  |  +--------------- bits 19-22: SHAPE_ID_HEAP_INDEX_MASK
               |  |  |  |  +------------------ bit 23: SHAPE_ID_FL_COMPLEX
               |  |  |  +--------------------- bit 24: SHAPE_ID_FL_FROZEN
               |  |  +------------------------ bit 25: SHAPE_ID_FL_HAS_OBJECT_ID
               +--+--------------------------- bits 26-27: SHAPE_ID_LAYOUT_MASK
```

The important part about these layout patterns is that they do not
reflect the _type_ of object, only how the object is laid out in memory.
For example, we currently treat structs as "other", but we can refactor
them to have the same layout as "Objects with fields_objects", and when
we do that they should get a different bit in the shape header.

This commit only reserves the two bits, it doesn't use them in the JIT
compiler yet.

Co-Authored-By: John Hawthorn <john@hawthorn.email>
Co-Authored-By: Max Bernstein <tekknolagi@gmail.com>

* Update gc.c

Co-authored-by: Nobuyoshi Nakada <nobu.nakada@gmail.com>

* Update shape.h

Co-authored-by: Jean Boussier <jean.boussier@gmail.com>

* fix function name

* Update shape.c

Co-authored-by: Jean Boussier <jean.boussier@gmail.com>

* fix function name

* Revert "Update shape.c"

This reverts commit 900711defc6c541a93f3393a350819ae88cf87f1.

* add comment

---------

Co-authored-by: John Hawthorn <john@hawthorn.email>
Co-authored-by: Max Bernstein <tekknolagi@gmail.com>
Co-authored-by: Nobuyoshi Nakada <nobu.nakada@gmail.com>
Co-authored-by: Jean Boussier <jean.boussier@gmail.com>
2026-05-29 15:09:34 -07:00
John Hawthorn 594ae9f073 Give legacy T_DATA the same layout as TypedData
This allows us to treat the two types of T_DATA the same way when
reading instance variables.

Non-TypedData T_DATA are deprecated, so making them faster isn't
particularly important, but it is helpful to avoid needing to test for
TypedData and adding another special case.
2026-05-27 16:18:43 -07:00
Jean Boussier deceb9f62e Rename rb_shape_obj_* into rb_obj_shape_*
For consistency.
2026-05-06 10:10:16 +02:00
Jean Boussier 4eeec614bb shapes: Rename TOO_COMPLEX in just COMPLEX
The `too_` prefix wasn't consistently used and just make the
thing longer for no benefit.
2026-05-06 10:10:16 +02:00
Jean Boussier 678d86f5e4 shape.c: transition to complex when max_capacity is reached
Now that we have 1024B slots, we can store up to 126 fields inline.

Objects larger than this are rare if not non-existent, hence we can
get rid of the `malloc` path for imemo/fields and simply transition
to `TOO_COMPLEX`.

This additionally allows to shrink `attr_index_t` from 16 to 8B.

Note: the ZJIT "ivar on extended" tests are renamed as "complex" because
"extended" AKA malloc allocated imemo/fields no longer exists.
They're now complex fields, AKA st tables.

rb_class_allocate_instance: start as complex when over max_fields

If `RCLASS_MAX_IV_COUNT` is over `max_fields`, allocating a large
slot to end up transitioning to `TOO_COMPLEX` is wasteful.

We might as well start as complex directly.
2026-05-06 09:02:02 +02:00
Jean Boussier 2f5b746454 Refactor setivar caches to include the source and destination shapes
This is how caches used to work prior to 913979bede,
however since inline cache need to be set atomically, they are
limited to 64bits, which wasn't enough to store an `attr_index_t`
and two `shape_id_t`.

But now that we only cache the shape's offset, which is only 19B,
we can store both the source and destination offsets along with
the field index.

Thanks to this, the cache validation is now much simpler, we
only have to do an exact match on the source offset, whereas
before if it wasn't an exact match, we had to try to match
the parent, then check the field name.

We however still have to validate the shape capacity, as
the fast path doesn't handle resizes, but that could be
handled in a followup.
2026-05-05 21:08:44 +02:00
Jean Boussier 6074e53703 Reduce INVALID_SHAPE_ID to only its offset
Given we only store the shape offset in ivar caches, and the
offset is only 19bits, we should be able to store two shape offsets
and an attribute index in a single uint64_t.

Which is perfect for `setivar` caches.

However when initializing caches we set them to `INVALID_SHAPE_ID`
which is a full 32bits.
By making `INVALID_SHAPE_ID` only as large as a shape offset
we open the door to packing more information in ivar caches.
2026-05-05 16:59:58 +02:00
Jean Boussier 1eda917b09 shape.c: finalize the "offset" rename
To better distinguish fully formed shape ids from "naked" offsets.
2026-05-05 10:52:20 +02:00
Jean Boussier a2ba1ed43b vm_insnhelper.c: refactor and optimize setivar cache revalidation
By only storing shape offsets in the cache, we're able to still
match two equal objects that happen to be in different heaps, as
well as to validate the object is neither frozen nor complex.
2026-05-04 17:41:37 +02:00
Jean Boussier 8d40d48148 vm_insnhelper.c: share the cache revalidation logic
It's the same code duplicated in 3 places.
2026-05-04 17:41:37 +02:00
Jean Boussier a57aba16cc shape.h: also ignore obj_id flag when reading ivars
It doesn't impact ivar lookup, because OBJ_ID fields change
the offset.
2026-05-03 23:40:36 +02:00
Jean Boussier beffa58b65 shape: Get rid of the "raw_id" terminology in favor of "offset"
It better describe that part of a `shape_id`.
2026-05-03 23:40:36 +02:00
Peter Zhu 39257a3e11 Add field in RObject for too complex hash case
Avoids the need to cast fields into a st_table.
2026-05-03 10:04:57 -04:00
Jean Boussier c9b53473e3 Refactor shape transition functions
Expose both `rb_obj_shape_` functions that take a `VALUE`
and `rb_shape_` functions that take a `shape_id`.

Make common transition functions such as `complex` and `frozen`
inlineable.

Also get rid of RB_SET_SHAPE_ID and rb_set_boxed_class_shape_id.
2026-05-03 11:36:26 +02:00
Jean Boussier 9c07713bcf shape.h: store ancestor_index as an offset
This shrinks the `rb_shape_t` struct from 40B to 32B.
2026-05-02 12:18:10 +02:00
Jean Boussier 3871175823 shape.h: declate rb_shape_t.type as enum 2026-05-02 12:18:10 +02:00
Jean Boussier 9afb8a65eb shape.c: reorganize rb_shape_tree_t
Embed and shrink the capacities array so that it can be queried
more efficiently.

Move `next_shape_id` and `cache_size` outside of the struct on their
own cache line, as they're expected to be incremented atomically from
concurrent threads, so we want to avoid false sharing.
The also don't need to be exposed to the rest of the VM.

Get rid of `root_shape` as it's always equal to `shape_list`.
2026-05-02 11:38:42 +02:00
Matt Valentine-House 2567e76ec3 Handle small pools in shape capacity calculation
When pool slot sizes can be smaller than sizeof(struct RBasic) (e.g. a
32-byte pool on 64-bit where RBasic is 16 bytes), the capacity
calculation would underflow. Guard against this by setting capacity to
0 for pools too small to hold fields.
2026-04-09 13:24:09 +01:00
Matt Valentine-House f46eb48b26 Expand the Shape heap index mask
I think we have spare bits here, so let's expand them out and see what
happens. This will allow us to have more than 7 size pools in the future
2026-03-03 21:00:57 +00:00
Peter Zhu 7379b9ed78 Optimize rb_mark_generic_ivar for T_DATA and T_STRUCT
T_DATA and T_STRUCT could have ivars but might not use the generic_fields_tbl.
This commit skips lookup in the generic_fields_tbl for those cases.
2026-01-09 17:31:10 +01:00
Peter Zhu 791acc5697 Revert "gc.c: Pass shape_id to newobj_init"
This reverts commit 228d13f6ed.

This commit makes default.c and mmtk.c depend on shape.h, which prevents
them from building independently.
2025-12-05 15:40:39 -08:00
Max Bernstein b43e66d3b3 YJIT: Pass class and shape ID directly instead of object 2025-12-03 16:59:05 -05:00
Jean Boussier 228d13f6ed gc.c: Pass shape_id to newobj_init
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.
2025-12-03 19:51:48 +01:00
Jean Boussier 5770c186d1 Rename rb_obj_exivar_p -> rb_obj_gen_fields_p
The "EXIVAR" terminology has been replaced by "gen fields"
AKA "generic fields".

Exivar implies variable, but generic fields include more than
just variables, e.g. `object_id`.
2025-12-03 15:57:26 +01:00
Jean Boussier 8c3909935e Handle NEWOBJ tracepoints settings fields
[Bug #21710]

- struct.c: `struct_alloc`

It is possible for a `NEWOBJ` tracepoint call back to write fields
into a newly allocated object before `struct_alloc` had the time
to set the `RSTRUCT_GEN_FIELDS` flags and such.

Hence we can't blindly initialize the `fields_obj` reference to `0`
we first need to check no fields were added yet.

- object.c: `rb_class_allocate_instance`

Similarly, if a `NEWOBJ` tracepoint tries to set fields on the object,
the `shape_id` must already be set, as it's required on T_OBJECT to
know where to write fields.

`NEWOBJ_OF` had to be refactored to accept a `shape_id`.
2025-12-03 08:14:56 +01:00
Luke Gruber e02eda194f Speedup RBASIC_FIELDS_COUNT (#15273)
We know the argument is not a class, module or special const, so we can
skip these checks.
2025-12-01 12:55:39 -05:00