ruby/test/ruby/test_object_id.rb
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

303 lines
7.1 KiB
Ruby

require 'test/unit'
require "securerandom"
class TestObjectId < Test::Unit::TestCase
def setup
@obj = Object.new
end
def test_dup_new_id
id = @obj.object_id
refute_equal id, @obj.dup.object_id
end
def test_dup_with_ivar_and_id
id = @obj.object_id
@obj.instance_variable_set(:@foo, 42)
copy = @obj.dup
refute_equal id, copy.object_id
assert_equal 42, copy.instance_variable_get(:@foo)
end
def test_dup_with_id_and_ivar
@obj.instance_variable_set(:@foo, 42)
id = @obj.object_id
copy = @obj.dup
refute_equal id, copy.object_id
assert_equal 42, copy.instance_variable_get(:@foo)
end
def test_dup_with_id_and_ivar_and_frozen
@obj.instance_variable_set(:@foo, 42)
@obj.freeze
id = @obj.object_id
copy = @obj.dup
refute_equal id, copy.object_id
assert_equal 42, copy.instance_variable_get(:@foo)
refute_predicate copy, :frozen?
end
def test_clone_new_id
id = @obj.object_id
refute_equal id, @obj.clone.object_id
end
def test_clone_with_ivar_and_id
id = @obj.object_id
@obj.instance_variable_set(:@foo, 42)
copy = @obj.clone
refute_equal id, copy.object_id
assert_equal 42, copy.instance_variable_get(:@foo)
end
def test_clone_with_id_and_ivar
@obj.instance_variable_set(:@foo, 42)
id = @obj.object_id
copy = @obj.clone
refute_equal id, copy.object_id
assert_equal 42, copy.instance_variable_get(:@foo)
end
def test_clone_with_id_and_ivar_and_frozen
@obj.instance_variable_set(:@foo, 42)
@obj.freeze
id = @obj.object_id
copy = @obj.clone
refute_equal id, copy.object_id
assert_equal 42, copy.instance_variable_get(:@foo)
assert_predicate copy, :frozen?
end
def test_marshal_new_id
return pass if @obj.is_a?(Module)
id = @obj.object_id
refute_equal id, Marshal.load(Marshal.dump(@obj)).object_id
end
def test_marshal_with_ivar_and_id
return pass if @obj.is_a?(Module)
id = @obj.object_id
@obj.instance_variable_set(:@foo, 42)
copy = Marshal.load(Marshal.dump(@obj))
refute_equal id, copy.object_id
assert_equal 42, copy.instance_variable_get(:@foo)
end
def test_marshal_with_id_and_ivar
return pass if @obj.is_a?(Module)
@obj.instance_variable_set(:@foo, 42)
id = @obj.object_id
copy = Marshal.load(Marshal.dump(@obj))
refute_equal id, copy.object_id
assert_equal 42, copy.instance_variable_get(:@foo)
end
def test_marshal_with_id_and_ivar_and_frozen
return pass if @obj.is_a?(Module)
@obj.instance_variable_set(:@foo, 42)
@obj.freeze
id = @obj.object_id
copy = Marshal.load(Marshal.dump(@obj))
refute_equal id, copy.object_id
assert_equal 42, copy.instance_variable_get(:@foo)
refute_predicate copy, :frozen?
end
def test_object_id_need_resize
(3 - @obj.instance_variables.size).times do |i|
@obj.instance_variable_set("@a_#{i}", "[Bug #21445]")
end
@obj.object_id
GC.start
end
end
class TestObjectIdClass < TestObjectId
def setup
@obj = Class.new
end
end
class TestObjectIdGeneric < TestObjectId
def setup
@obj = Array.new
end
end
class TestObjectIdTooComplex < TestObjectId
class TooComplex
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|
TooComplex.new.instance_variable_set("@TestObjectIdTooComplex#{i}", 1)
end
@obj = TooComplex.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
class TestObjectIdTooComplexClass < TestObjectId
class TooComplex < Module
end
def setup
if defined?(RubyVM::Shape::SHAPE_MAX_VARIATIONS)
assert_equal 8, RubyVM::Shape::SHAPE_MAX_VARIATIONS
end
@obj = TooComplex.new
@obj.instance_variable_set("@___#{SecureRandom.hex}", 1)
8.times do |i|
@obj.instance_variable_set("@TestObjectIdTooComplexClass#{i}", 1)
@obj.remove_instance_variable("@TestObjectIdTooComplexClass#{i}")
end
@obj.instance_variable_set("@test", 1)
if defined?(RubyVM::Shape)
assert_predicate(RubyVM::Shape.of(@obj), :too_complex?)
end
end
end
class TestObjectIdTooComplexGeneric < TestObjectId
class TooComplex < Array
end
def setup
if defined?(RubyVM::Shape::SHAPE_MAX_VARIATIONS)
assert_equal 8, RubyVM::Shape::SHAPE_MAX_VARIATIONS
end
8.times do |i|
TooComplex.new.instance_variable_set("@TestObjectIdTooComplexGeneric#{i}", 1)
end
@obj = TooComplex.new
@obj.instance_variable_set("@a#{rand(10_000)}", 1)
@obj.instance_variable_set("@a#{rand(10_000)}", 1)
if defined?(RubyVM::Shape)
assert_predicate(RubyVM::Shape.of(@obj), :too_complex?)
end
end
end
class TestObjectIdRactor < Test::Unit::TestCase
def test_object_id_race_free
assert_separately([], "#{<<~"begin;"}\n#{<<~'end;'}")
begin;
Warning[:experimental] = false
class MyClass
attr_reader :a, :b, :c
def initialize
@a = @b = @c = nil
end
end
N = 10_000
objs = Ractor.make_shareable(N.times.map { MyClass.new })
results = 4.times.map{
Ractor.new(objs) { |objs|
vars = []
ids = []
objs.each do |obj|
vars << obj.a << obj.b << obj.c
ids << obj.object_id
end
[vars, ids]
}
}.map(&:value)
assert_equal 1, results.uniq.size
end;
end
def test_external_object_id_ractor_move
assert_separately([], "#{<<~"begin;"}\n#{<<~'end;'}")
begin;
Warning[:experimental] = false
class MyClass
attr_reader :a, :b, :c
def initialize
@a = @b = @c = nil
end
end
obj = Ractor.make_shareable(MyClass.new)
object_id = obj.object_id
obj = Ractor.new { Ractor.receive }.send(obj, move: true).value
assert_equal object_id, obj.object_id
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