mirror of
https://github.com/ruby/ruby.git
synced 2025-08-15 13:39:04 +02:00

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>
303 lines
7.1 KiB
Ruby
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
|