Commit graph

820 commits

Author SHA1 Message Date
Jean Boussier
2083fa89fc Implement gen_fields_tbl cache
There is a high likelyhood that `rb_obj_fields` is called
consecutively for the same object.

If we keep a cache of the last IMEMO/fields we interacted with,
we can save having to lookup the `gen_fields_tbl`, synchronize
the VM lock, etc.

On yjit-bench's, I instrumented the hit rate of this cache at:

  - `shipit`: 38%, with 111k hits.
  - `lobsters`: 59%, with 367k hits.
  - `rubocop`: 100% with only 300 hits.

I also ran a micro-benchmark which shows that ivar access is:

  - 1.25x faster when the cache is hit in single ractor mode.
  - 2x faster when the cache is hit in multi ractor mode.
  - 1.06x slower when the cache miss in single ractor mode.
  - 1.01x slower when the cache miss in multi ractor mode.

```yml
prelude: |
  class GenIvar < Array
    def initialize(...)
      super
      @iv = 1
    end

    attr_reader :iv
  end

  a = GenIvar.new
  b = GenIvar.new
benchmark:
  hit: a.iv; a.iv; a.iv; a.iv; a.iv; a.iv; a.iv; a.iv; a.iv; a.iv; a.iv; a.iv; a.iv; a.iv; a.iv; a.iv; a.iv; a.iv; a.iv; a.iv;
  miss: a.iv; b.iv; a.iv; b.iv; a.iv; b.iv; a.iv; b.iv; a.iv; b.iv; a.iv; b.iv; a.iv; b.iv; a.iv; b.iv; a.iv; b.iv; a.iv; b.iv;
```

Single ractor:
```
compare-ruby: ruby 3.5.0dev (2025-08-12T02:14:57Z master 428937a536) +YJIT +PRISM [arm64-darwin24]
built-ruby: ruby 3.5.0dev (2025-08-12T09:25:35Z gen-fields-cache 9456c35893) +YJIT +PRISM [arm64-darwin24]
warming up..

|      |compare-ruby|built-ruby|
|:-----|-----------:|---------:|
|hit   |      4.090M|    5.121M|
|      |           -|     1.25x|
|miss  |      3.756M|    3.534M|
|      |       1.06x|         -|
```

Multi-ractor:
```
compare-ruby: ruby 3.5.0dev (2025-08-12T02:14:57Z master 428937a536) +YJIT +PRISM [arm64-darwin24]
built-ruby: ruby 3.5.0dev (2025-08-12T09:25:35Z gen-fields-cache 9456c35893) +YJIT +PRISM [arm64-darwin24]
warming up..

|      |compare-ruby|built-ruby|
|:-----|-----------:|---------:|
|hit   |      2.205M|    4.460M|
|      |           -|     2.02x|
|miss  |      2.117M|    2.094M|
|      |       1.01x|         -|
```
2025-08-13 19:54:56 +02:00
Jean Boussier
10aa4134d4 imemo_fields: store owner object in RBasic.klass
It is much more convenient than storing the klass, especially
when dealing with `object_id` as it allows to update the id2ref
table without having to dereference the owner, which may be
garbage at that point.
2025-08-13 19:53:18 +02:00
Jean Boussier
360be94d04 RTypedData: keep direct reference to IMEMO/fields
Similar to f3206cc79b but for TypedData.

It's quite common for TypedData objects to have a mix of reference in
their struct and some ivars.
Since we do happen to have 8B free in the RtypedData struct, we could
use it to keep a direct reference to the IMEMO/fields saving having
to synchronize the VM and lookup the `gen_fields_tbl` on every ivar
access.

For old school Data classes however, we don't have free space, but
this API is soft-deprecated and no longer very common.
2025-08-12 21:57:16 +02:00
Peter Zhu
814eaf336a Use rb_gc_mark_and_move for autoload_data 2025-08-12 10:12:19 -04:00
Peter Zhu
0019e7c702 Use rb_gc_mark_and_move for autoload_const 2025-08-12 10:12:19 -04:00
Jean Boussier
f3206cc79b Struct: keep direct reference to IMEMO/fields when space allows
It's not rare for structs to have additional ivars, hence are one
of the most common, if not the most common type in the `gen_fields_tbl`.

This can cause Ractor contention, but even in single ractor mode
means having to do a hash lookup to access the ivars, and increase
GC work.

Instead, unless the struct is perfectly right sized, we can store
a reference to the associated IMEMO/fields object right after the
last struct member.

```
compare-ruby: ruby 3.5.0dev (2025-08-06T12:50:36Z struct-ivar-fields-2 9a30d141a1) +PRISM [arm64-darwin24]
built-ruby: ruby 3.5.0dev (2025-08-06T12:57:59Z struct-ivar-fields-2 2ff3ec237f) +PRISM [arm64-darwin24]
warming up.....

|                      |compare-ruby|built-ruby|
|:---------------------|-----------:|---------:|
|member_reader         |    590.317k|  579.246k|
|                      |       1.02x|         -|
|member_writer         |    543.963k|  527.104k|
|                      |       1.03x|         -|
|member_reader_method  |    213.540k|  213.004k|
|                      |       1.00x|         -|
|member_writer_method  |    192.657k|  191.491k|
|                      |       1.01x|         -|
|ivar_reader           |    403.993k|  569.915k|
|                      |           -|     1.41x|
```

Co-Authored-By: Étienne Barrié <etienne.barrie@gmail.com>
2025-08-06 17:07:49 +02:00
Jean Boussier
92688f7d57 variable.c: refactor accesses to the generic_fields_tbl
All accesses to `generic_fields_tbl_` are now encapsulated inside:

  - `rb_obj_fields`
  - `rb_obj_set_fields`
  - `rb_obj_replace_fields`
2025-08-06 12:33:44 +02:00
Jean Boussier
fca2e6f8f5 variable.c: Fix rb_ivar_foreach to not yield object_id of complex objects
Co-Authored-By: Étienne Barrié <etienne.barrie@gmail.com>
2025-08-01 18:00:05 +02:00
Alan Wu
5a7be72c06 Also include shape type in assert
The assert also fails on interpreter-only runs:
4714861028
2025-07-31 16:23:24 -04:00
Alan Wu
84ee71df45 Print shape id when a flaky shapes assert fails
This has been failing intermittently during
`test_marshal_with_ivar_and_id` for ZJIT and YJIT builds.

4714097060
4714140911
2025-07-31 15:30:46 -04:00
Luke Gruber
be58cd4d7d Ractor: lock around global variable get/set
There's a global id_table `rb_global_tbl` that needs a lock (I used VM lock). In the future, we might use a lock-free rb_id_table if we create such a data structure.

Reproduction script that might crash or behave strangely:

```ruby
100.times do
  Ractor.new do
    1_000_000.times do
      $stderr
      $stdout
      $stdin
      $VERBOSE
      $stderr
      $stdout
      $stdin
      $VERBOSE
      $stderr
      $stdout
      $stdin
      $VERBOSE
    end
  end
end

$myglobal0 = nil;
$myglobal1 = nil;
  # ... vim macros to the rescue
$myglobal100000 = nil;
```
2025-07-21 15:57:44 +02:00
Nobuyoshi Nakada
55baf026ac
Fix an indent [ci skip] 2025-07-14 11:19:10 +09:00
John Hawthorn
581da51cb5 Fix whitespace on some RB_VM_LOCKING calls 2025-07-09 17:28:47 -07:00
John Hawthorn
12b0ce3875 Remove unused src param from rb_shape_copy_fields 2025-07-04 14:54:49 -07:00
John Hawthorn
8cd5832694 Fix wrong write barrier on fields copy
Previously this write barrier was using the destination object as the
new parent, rather than the fields object.

Found by wbcheck
2025-07-04 14:54:49 -07:00
John Hawthorn
32453560de Fix missed write barrier on Ractor send move
When moving a "generic IV" object, we need a write barrier to the fields
object.

   WBCHECK ERROR: Missed write barrier detected!
     Parent object: 0x7c913641d1a0 (wb_protected: true)
       rb_obj_info_dump: 0x00007c913641d1a0 T_ARRAY/Array [E ] len: 10 (embed)
     Reference counts - snapshot: 1, writebarrier: 0, current: 2, missed: 1
     Missing reference to: 0x7bf1364e56d0
       rb_obj_info_dump: 0x00007bf1364e56d0 T_IMEMO/<fields>
2025-07-04 14:54:49 -07:00
Jean Boussier
517c106709 imemo_fields_set: save copying when reassigning a variable
If we still fit in the existing imemo/fields object we can
update it atomically, saving a reallocation.
2025-07-03 09:20:22 +02:00
Jean Boussier
9ab3e47d35 Simplify rb_fields_tbl_copy
Now that ivars are stored in a imemo/fields, we can just clone
the fields object.
2025-07-01 12:39:23 +02:00
Jean Boussier
00357eea31 class_fields_ivar_set: fix multi-ractor mode
We must copy the table before inserting into it if we're in
multi-ractor mode to ensure the table won't be rehashed or
resized.
2025-06-30 11:11:03 +02:00
Jean Boussier
879f4886c9 variable.c: Extract imemo_fields_copy_capa
This code is similar between classes and generic ivars.
2025-06-30 11:11:03 +02:00
Jean Boussier
e06c74e5f7 Refactor class_fields_ivar_set to use imemo_fields_complex_from_obj 2025-06-30 11:11:03 +02:00
Jean Boussier
242343ff80 variable.c: Refactor generic_field_set / generic_ivar_set
These two functions are very similar, they can share most of their
logic.
2025-06-26 16:25:57 +02:00
John Hawthorn
363239585d Fix missing WB going to too_complex on class/geniv
We were creating a new fields object, and then inserting into it without
a write barrier.

    OpenSSL::TestSSL#test_tlsext_hostname
    WBCHECK ERROR: Missed write barrier detected!
      Parent object: 0x7ce8b3a390c0 (wb_protected: true)
        rb_obj_info_dump: 0x00007ce8b3a390c0 T_IMEMO/<fields>
      Reference counts - snapshot: 1, writebarrier: 1, current: 4, missed: 2
      Missing reference to: 0x7c08b4110750
        rb_obj_info_dump: 0x00007c08b4110750 OpenSSL/X509/OpenSSL::X509::Certificate OpenSSL/X509
      Missing reference to: 0x7c08b4128dd0
        rb_obj_info_dump: 0x00007c08b4128dd0 OpenSSL/EVP_PKEY/OpenSSL::PKey::RSA OpenSSL/EVP_PKEY
2025-06-25 13:06:02 +02:00
Jean Boussier
45a2c95d0f Reduce exposure of FL_FREEZE
The `FL_FREEZE` flag is redundant with `SHAPE_ID_FL_FROZEN`, so
ideally it should be eliminated in favor of the later.

Doing so would eliminate the risk of desync between the two, but
also solve the problem of the frozen status being global in namespace
context (See Bug #21330).
2025-06-24 11:29:39 +01:00
Jean Boussier
edbd9ed468 variable.c: avoid out of bound write in generic_field_set
[Bug #21445]
2025-06-21 14:40:40 +01:00
John Hawthorn
912edb4716 Fix missing write barrier on class fields
Found by wbcheck

    klass = Class.new
    200.times do |iv|
      klass.instance_variable_set("@_iv_#{iv}", Object.new)
    end
2025-06-18 20:54:01 -07:00
Jean Boussier
8faa32327b Add missing write barriers in rb_imemo_fields_clone. 2025-06-17 15:28:05 +02:00
Jean Boussier
cd9f447be2 Refactor generic fields to use T_IMEMO/fields objects.
Followup: https://github.com/ruby/ruby/pull/13589

This simplify a lot of things, as we no longer need to manually
manage the memory, we can use the Read-Copy-Update pattern and
avoid numerous race conditions.

Co-Authored-By: Étienne Barrié <etienne.barrie@gmail.com>
2025-06-17 15:28:05 +02:00
Jean Boussier
164486a954 Refactor rb_imemo_fields_new to not assume T_CLASS 2025-06-17 15:28:05 +02:00
Jean Boussier
fb68721f63 Rename imemo_class_fields -> imemo_fields 2025-06-17 15:28:05 +02:00
Nobuyoshi Nakada
8992689118
Adjust indent [ci] 2025-06-17 16:30:40 +09:00
John Hawthorn
055fef00a1 Free after insert in generic_ivar_set_shape_fields
Previously we were performing a realloc and then inserting the new value
into the table. If the table was flagged as requiring a rebuild, this
could trigger GC work and marking within that GC could access the fields
freed by realloc.
2025-06-17 08:01:23 +02:00
John Hawthorn
39697ffd01 Remove fields_tbl in gen_fields_lookup_ensure_size 2025-06-13 23:29:41 -07:00
John Hawthorn
5342d9130b Fix generic_ivar_set_shape_field for table rebuild
[Bug #21438]

Previously GC could trigger a table rebuild of the generic fields
st_table in the middle of calling the st_update callback. This could
cause entries to be reallocated or rearranged and the update to be for
the wrong entry.

This commit adds an assertion to make that case easier to detect, and
replaces the st_update with a separate st_lookup and st_insert.

Co-authored-by: Aaron Patterson <tenderlove@ruby-lang.org>
Co-authored-by: Jean Boussier <byroot@ruby-lang.org>
2025-06-13 23:29:41 -07:00
Jean Boussier
15084fbc3c Get rid of FL_EXIVAR
Now that the shape_id gives us all the same information, it's no
longer needed.
2025-06-13 23:50:30 +02:00
Jean Boussier
6dbe24fe56 Use the shape_id rather than FL_EXIVAR
We still keep setting `FL_EXIVAR` so that `rb_shape_verify_consistency`
can detect discrepancies.
2025-06-13 23:50:30 +02:00
Jean Boussier
b51078f82e Enforce consistency between shape_id and FL_EXIVAR
The FL_EXIVAR is a bit redundant with the shape_id.
Now that the `shape_id` is embedded in all objects on all archs,
we can cheaply check if an object has any fields with a simple
bitmask.
2025-06-13 23:50:30 +02:00
Jean Boussier
a74c385208 Make setting and accessing class ivars lock-free
Now that class fields have been deletated to a T_IMEMO/class_fields
when we're in multi-ractor mode, we can read and write class instance
variable in an atomic way using Read-Copy-Update (RCU).

Note when in multi-ractor mode, we always use RCU. In theory
we don't need to, instead if we ensured the field is written
before the shape is updated it would be safe.

Benchmark:

```ruby
Warning[:experimental] = false

class Foo
  @foo = 1
  @bar = 2
  @baz = 3
  @egg = 4
  @spam = 5

  class << self
    attr_reader :foo, :bar, :baz, :egg, :spam
  end
end

ractors = 8.times.map do
  Ractor.new do
    1_000_000.times do
      Foo.bar + Foo.baz * Foo.egg - Foo.spam
    end
  end
end

if Ractor.method_defined?(:value)
  ractors.each(&:value)
else
  ractors.each(&:take)
end
```

This branch vs Ruby 3.4:

```bash
$ hyperfine -w 1 'ruby --disable-all ../test.rb' './miniruby ../test.rb'

Benchmark 1: ruby --disable-all ../test.rb
  Time (mean ± σ):      3.162 s ±  0.071 s    [User: 2.783 s, System: 10.809 s]
  Range (min … max):    3.093 s …  3.337 s    10 runs

Benchmark 2: ./miniruby ../test.rb
  Time (mean ± σ):     208.7 ms ±   4.6 ms    [User: 889.7 ms, System: 6.9 ms]
  Range (min … max):   202.8 ms … 222.0 ms    14 runs

Summary
  ./miniruby ../test.rb ran
   15.15 ± 0.47 times faster than ruby --disable-all ../test.rb
```
2025-06-12 14:55:13 +02:00
Jean Boussier
8b5ac5abf2 Fix class instance variable inside namespaces
Now that classes fields are delegated to an object with its own
shape_id, we no longer need to mark all classes as TOO_COMPLEX.
2025-06-12 13:43:29 +02:00
Jean Boussier
3abdd4241f Turn rb_classext_t.fields into a T_IMEMO/class_fields
This behave almost exactly as a T_OBJECT, the layout is entirely
compatible.

This aims to solve two problems.

First, it solves the problem of namspaced classes having
a single `shape_id`. Now each namespaced classext
has an object that can hold the namespace specific
shape.

Second, it open the door to later make class instance variable
writes atomics, hence be able to read class variables
without locking the VM.
In the future, in multi-ractor mode, we can do the write
on a copy of the `fields_obj` and then atomically swap it.

Considerations:

  - Right now the `RClass` shape_id is always synchronized,
    but with namespace we should likely mark classes that have
    multiple namespace with a specific shape flag.
2025-06-12 07:58:16 +02:00
Jean Boussier
95201299fd Refactor the last references to rb_shape_t
The type isn't opaque because Ruby isn't often compiled with LTO,
so for optimization purpose it's better to allow as much inlining
as possible.

However ideally only `shape.c` and `shape.h` should deal with
the actual struct, and everything else should just deal with opaque
`shape_id_t`.
2025-06-11 16:38:38 +02:00
Jean Boussier
f9966b9b76 Get rid of gen_fields_tbl.fields_count
This data is redundant because the shape already contains both the
length and capacity of the object's fields.

So it both waste space and create the possibility of a desync between
the two.

We also do not need to initialize everything to Qundef, this seem
to be a left-over from pre-shape instance variables.
2025-06-09 16:38:29 +02:00
Jean Boussier
6eb0cd8df7 Get rid of SHAPE_T_OBJECT
Now that we have the `heap_index` in shape flags we no longer
need `T_OBJECT` shapes.
2025-06-07 18:30:44 +02:00
Jean Boussier
8c4e368dcf shape.c: ensure heap_index is consistent for complex shapes 2025-06-07 18:30:44 +02:00
Jean Boussier
4e39580992 Refactor raw accesses to rb_shape_t.capacity 2025-06-05 22:06:15 +02:00
Nobuyoshi Nakada
edaa27ce45
Suppress warnings by gcc-13 with -Og 2025-06-05 22:33:02 +09:00
Jean Boussier
772fc1f187 Get rid of rb_shape_t.flags
Now all flags are only in the `shape_id_t`, and can all be checked
without needing to dereference a pointer.
2025-06-05 07:44:44 +02:00
Jean Boussier
675f33508c Get rid of TOO_COMPLEX shape type
Instead it's now a `shape_id` flag.

This allows to check if an object is complex without having
to chase the `rb_shape_t` pointer.
2025-06-04 13:13:50 +02:00
Jean Boussier
206110a2a8 Add missing lock in rb_ivar_defined
If called on a class, we should acquire the lock.
2025-06-04 08:26:20 +02:00
Jean Boussier
625d6a9cbb Get rid of frozen shapes.
Instead `shape_id_t` higher bits contain flags, and the first one
tells whether the shape is frozen.

This has multiple benefits:
  - Can check if a shape is frozen with a single bit check instead of
    dereferencing a pointer.
  - Guarantees it is always possible to transition to frozen.
  - This allow reclaiming `FL_FREEZE` (not done yet).

The downside is you have to be careful to preserve these flags
when transitioning.
2025-06-04 07:59:20 +02:00