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>
This commit is contained in:
Jean Boussier 2025-08-06 14:46:36 +02:00
parent 9b3ad3449b
commit f3206cc79b
8 changed files with 194 additions and 23 deletions

View file

@ -252,3 +252,52 @@ class TestObjectIdRactor < Test::Unit::TestCase
end;
end
end
class TestObjectIdStruct < TestObjectId
EmbeddedStruct = Struct.new(:embedded_field)
def setup
@obj = EmbeddedStruct.new
end
end
class TestObjectIdStructGenIvar < TestObjectId
GenIvarStruct = Struct.new(:a, :b, :c)
def setup
@obj = GenIvarStruct.new
end
end
class TestObjectIdStructNotEmbed < TestObjectId
MANY_IVS = 80
StructNotEmbed = Struct.new(*MANY_IVS.times.map { |i| :"field_#{i}" })
def setup
@obj = StructNotEmbed.new
end
end
class TestObjectIdStructTooComplex < TestObjectId
StructTooComplex = Struct.new(:a) do
def initialize
@too_complex_obj_id_test = 1
end
end
def setup
if defined?(RubyVM::Shape::SHAPE_MAX_VARIATIONS)
assert_equal 8, RubyVM::Shape::SHAPE_MAX_VARIATIONS
end
8.times do |i|
StructTooComplex.new.instance_variable_set("@TestObjectIdStructTooComplex#{i}", 1)
end
@obj = StructTooComplex.new
@obj.instance_variable_set("@a#{rand(10_000)}", 1)
if defined?(RubyVM::Shape)
assert_predicate(RubyVM::Shape.of(@obj), :too_complex?)
end
end
end

View file

@ -99,6 +99,24 @@ class TestRactor < Test::Unit::TestCase
RUBY
end
def test_struct_instance_variables
assert_ractor(<<~'RUBY')
StructIvar = Struct.new(:member) do
def initialize(*)
super
@ivar = "ivar"
end
attr_reader :ivar
end
obj = StructIvar.new("member")
obj_copy = Ractor.new { Ractor.receive }.send(obj).value
assert_equal obj.ivar, obj_copy.ivar
refute_same obj.ivar, obj_copy.ivar
assert_equal obj.member, obj_copy.member
refute_same obj.member, obj_copy.member
RUBY
end
def test_fork_raise_isolation_error
assert_ractor(<<~'RUBY')
ractor = Ractor.new do