264 Commits
Author SHA1 Message Date
Matt Valentine-House 5d3ac10186 Allow shapes to carry zero embedded capacity
rb_shape_verify_consistency asserted that every checked object has room
for at least one embedded field.

The default GC always satisfies this because its smallest slot is 32
bytes so this assertion never fires. MMTk gives an empty imemo_fields a
16-byte slot, so its shape carries zero capacity bits, and boot failed
on the first class freeze.

Instead of forcing MMTk to have a minimum object size, we should just
allow shapes to support a capacity of 0.
2026-08-19 17:14:06 +01: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 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 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 6f0548a0bd Simplify rb_obj_copy_ivar and rb_copy_generic_ivar
By handling complex shapes in `rb_shape_rebuild` we can merge
the two complex cases (when `src` is complex and when `dest`
becomes complex).
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 dac89892b9 gc: call rb_gc_obj_changed_slot_size for all types
Now that the shape contains the slot capacity, we should update
the shape for all objects when moving them, not just T_OBJECT.
2026-07-08 13:39:59 +02:00
Jean Boussier c966f3abd7 imemo.c: refactor imemo_fields_new
Lean on `rb_newobj` to get the shape embedded capacity properly
set.
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 cfb1a23188 Make rb_shape_get_root_shape static 2026-07-02 00:19:04 -04:00
Peter Zhu 07abf0b039 Remove function shape_get_next_iv_shape 2026-06-29 20:47:29 -04:00
Peter Zhu f8b25fe249 Remove dead rb_shape_get_next_iv_shape 2026-06-29 20:47:29 -04:00
John HawthornandLuke Gruber daffaaee73 Mark most TypedData types as THREAD_SAFE_FREE
Set RUBY_TYPED_THREAD_SAFE_FREE on TypedData types whose dfree function
is trivially safe, and only frees its own memory

Types not yet marked as THREAD_SAFE_FREE:
- id2ref_tbl_type: sets the process-global id2ref_tbl to NULL.
- mutex_data_type: unlinks itself from another thread's keeping_mutexes list.
- autoload_data_type: deletes nodes from a shared intrusive list.
- autoload_const_type: deletes itself from a shared list.
- rb_cont_data_type: mutates the shared fiber pool and the global first_jit_cont list.
- rb_fiber_data_type: delegates to cont_free.
- FiberPoolDataType: mutates shared fiber pool free-lists.
- ractor_data_type: reconfigures global VM event-hook flags and call caches.
- exported_object_registry: frees a global table under the VM lock.
- rb_box_data_type (Box::Entry): unlinks classext from other live class and module objects.
- box_ext_cleanup_type: dereferences another String object and calls unlink.
- monitor_data_type: uses the default free, so the flag has no effect.

Co-authored-by: Luke Gruber <luke.gruber@shopify.com>
2026-06-23 13:30:26 -07:00
Yusuke EndohandClaude Opus 4.8 a83e66f51a Transition to COMPLEX when object_id overflows the max fields
rb_shape_transition_object_id skipped the max_capacity guard that the
instance variable path applies, so an object filled to exactly
SHAPE_MAX_FIELDS would assert when assigning an object_id.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 20:54:19 +09:00
Alexander Momchilov d6aa40a83e Document fake pointer trick (#17253) 2026-06-10 18:34:50 +00: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
Nobuyoshi Nakada 186da0b481 Fix modifier for size_t 2026-05-10 19:56:16 +09: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 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 bd49c02e6e shape.c: rename shape_id and shape_offset helpers
These are natural variable names, so by using macro like
case we avoid name conflicts.
2026-05-05 10:52:20 +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 a3cfaf2503 test_shapes.rb: replace hardcoded MANY_IVS constant
Since more size pools have been added, this constant was outdated.
2026-05-04 10:33:29 +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
Jean Boussier 1455aa580f shape.c: Add a fastpath in rb_shape_rebuild
When the shape doesn't have a SHAPE_OBJ_ID edge, we don't
really have to rebuild it, we can simply combine the old shape
offset with the new shape flags.
2026-05-03 23:40:36 +02: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 87529e1961 shape.c: simplify transition_complex
We don't need to call into `shape_transition_object_id`, we know
we're transitioning to TOO_COMPLEX so the offset part of the id
can only be `ROOT_TOO_COMPLEX_SHAPE_ID` or `ROOT_TOO_COMPLEX_WITH_OBJ_ID`.

So there's no need to lookup the shape tree.
2026-05-03 10:00:03 +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 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
Jean Boussier 45d586ad60 Never increment max_iv_count on Object / BasicObject.
Otherwise some code defining ivars on a naked object early during
program boot can cause all objects to be larger than needed.

This seem like it was always the intention, but wasn't quite
properly prevented.

Also stop updating `max_iv_count` during GC marking as it is
redundant.
2026-05-02 08:46:36 +02:00
Matt Valentine-House c919778017 Remove extra sentinel from shape capacities
This isn't a 0 terminated list anymore because we iterate over
heaps_count directly. So we don't need to allocate an extra byte for the
sentinel
2026-04-09 13:24:09 +01: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
Max Bernstein b43e66d3b3 YJIT: Pass class and shape ID directly instead of object 2025-12-03 16:59:05 -05:00
Max Bernstein f167073324 Move imemo fields check out of shape_get_next
Not every caller (for example, YJIT) actually needs to pass the object.
YJIT (and, in the future, ZJIT) only need to pass the class.
2025-12-03 16:59:05 -05:00
Max BernsteinandAaron Patterson 612a66805f Remove spurious obj != klass check in shape_get_next
This should never be true. I added an `rb_bug` in case it was and it
wasn't true in any of btest or test-all.

Co-authored-by: Aaron Patterson <tenderlove@ruby-lang.org>
2025-12-03 16:59:05 -05: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
Satoshi Tagomori d2a587c791 renaming internal data structures and functions from namespace to box 2025-11-07 13:14:54 +09:00
Jean Boussier 05fc01188a rb_ivar_delete: allow complex transition
`rb_ivar_delete` would force a new shape to be created.
And in the case of a shape exhaustion, it wouldn't handle
T_CLASS/T_MODULE correctly.
2025-08-28 15:21:05 +02:00
Jean Boussier 5257e1298c Replace ROBJECT_EMBED by ROBJECT_HEAP
The embed layout is way more common than the heap one,
especially since WVA.

I think it makes for more readable code to inverse the
flag.
2025-08-27 12:41:07 +02:00
Jean Boussier 14bdf4b57d Ensure T_OBJECT and T_IMEMO/fields have identical layout 2025-08-26 13:44:59 +02:00
Étienne Barrié b0c80c2be8 Remove unused SPECIAL_CONST_SHAPE_ID
Its usage was removed in 306d50811d.
2025-08-21 17:41:39 +02:00
Max Bernstein 306d50811d Don't allow looking at the shape ID of immediates (#14266)
It only makes sense for heap objects.
2025-08-18 18:58:38 -04:00
John HawthornandAaron Patterson 7d2b724e08 Add missing writebarrier on complex obj dup
When we dup a complex object we need to issue a writebarrier_remember
on the new object. This was caught by wbcheck inside the rubygems test
suite.

Co-authored-by: Aaron Patterson <tenderlove@ruby-lang.org>
2025-08-18 11:59:58 -07:00