Skip to content

Commit f3206cc

Browse files
byrootetiennebarrie
andcommitted
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é <[email protected]>
1 parent 9b3ad34 commit f3206cc

File tree

8 files changed

+194
-23
lines changed

8 files changed

+194
-23
lines changed

benchmark/struct_accessor.yml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
prelude: |
22
C = Struct.new(:x) do
3+
def initialize(...)
4+
super
5+
@ivar = 42
6+
end
7+
8+
attr_accessor :ivar
9+
310
class_eval <<-END
411
def r
512
#{'x;'*256}
@@ -15,11 +22,16 @@ prelude: |
1522
m = method(:x=)
1623
#{'m.call(nil);'*256}
1724
end
25+
def r_ivar
26+
#{'ivar;'*256}
27+
end
1828
END
1929
end
30+
C.new(nil) # ensure common shape is known
2031
obj = C.new(nil)
2132
benchmark:
2233
member_reader: "obj.r"
2334
member_writer: "obj.w"
2435
member_reader_method: "obj.rm"
2536
member_writer_method: "obj.wm"
37+
ivar_reader: "obj.r_ivar"

depend

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6065,6 +6065,7 @@ hash.$(OBJEXT): $(top_srcdir)/internal/set_table.h
60656065
hash.$(OBJEXT): $(top_srcdir)/internal/st.h
60666066
hash.$(OBJEXT): $(top_srcdir)/internal/static_assert.h
60676067
hash.$(OBJEXT): $(top_srcdir)/internal/string.h
6068+
hash.$(OBJEXT): $(top_srcdir)/internal/struct.h
60686069
hash.$(OBJEXT): $(top_srcdir)/internal/symbol.h
60696070
hash.$(OBJEXT): $(top_srcdir)/internal/thread.h
60706071
hash.$(OBJEXT): $(top_srcdir)/internal/time.h
@@ -6288,6 +6289,7 @@ hash.$(OBJEXT): {$(VPATH)}symbol.h
62886289
hash.$(OBJEXT): {$(VPATH)}thread_$(THREAD_MODEL).h
62896290
hash.$(OBJEXT): {$(VPATH)}thread_native.h
62906291
hash.$(OBJEXT): {$(VPATH)}util.h
6292+
hash.$(OBJEXT): {$(VPATH)}variable.h
62916293
hash.$(OBJEXT): {$(VPATH)}vm_core.h
62926294
hash.$(OBJEXT): {$(VPATH)}vm_debug.h
62936295
hash.$(OBJEXT): {$(VPATH)}vm_opts.h
@@ -12926,6 +12928,7 @@ range.$(OBJEXT): $(top_srcdir)/internal/enumerator.h
1292612928
range.$(OBJEXT): $(top_srcdir)/internal/error.h
1292712929
range.$(OBJEXT): $(top_srcdir)/internal/fixnum.h
1292812930
range.$(OBJEXT): $(top_srcdir)/internal/gc.h
12931+
range.$(OBJEXT): $(top_srcdir)/internal/imemo.h
1292912932
range.$(OBJEXT): $(top_srcdir)/internal/numeric.h
1293012933
range.$(OBJEXT): $(top_srcdir)/internal/range.h
1293112934
range.$(OBJEXT): $(top_srcdir)/internal/serial.h
@@ -12948,6 +12951,7 @@ range.$(OBJEXT): {$(VPATH)}config.h
1294812951
range.$(OBJEXT): {$(VPATH)}defines.h
1294912952
range.$(OBJEXT): {$(VPATH)}encoding.h
1295012953
range.$(OBJEXT): {$(VPATH)}id.h
12954+
range.$(OBJEXT): {$(VPATH)}id_table.h
1295112955
range.$(OBJEXT): {$(VPATH)}intern.h
1295212956
range.$(OBJEXT): {$(VPATH)}internal.h
1295312957
range.$(OBJEXT): {$(VPATH)}internal/abi.h
@@ -15580,6 +15584,7 @@ shape.$(OBJEXT): $(top_srcdir)/internal/serial.h
1558015584
shape.$(OBJEXT): $(top_srcdir)/internal/set_table.h
1558115585
shape.$(OBJEXT): $(top_srcdir)/internal/static_assert.h
1558215586
shape.$(OBJEXT): $(top_srcdir)/internal/string.h
15587+
shape.$(OBJEXT): $(top_srcdir)/internal/struct.h
1558315588
shape.$(OBJEXT): $(top_srcdir)/internal/symbol.h
1558415589
shape.$(OBJEXT): $(top_srcdir)/internal/variable.h
1558515590
shape.$(OBJEXT): $(top_srcdir)/internal/vm.h
@@ -16569,6 +16574,7 @@ string.$(OBJEXT): $(top_srcdir)/internal/serial.h
1656916574
string.$(OBJEXT): $(top_srcdir)/internal/set_table.h
1657016575
string.$(OBJEXT): $(top_srcdir)/internal/static_assert.h
1657116576
string.$(OBJEXT): $(top_srcdir)/internal/string.h
16577+
string.$(OBJEXT): $(top_srcdir)/internal/struct.h
1657216578
string.$(OBJEXT): $(top_srcdir)/internal/transcode.h
1657316579
string.$(OBJEXT): $(top_srcdir)/internal/variable.h
1657416580
string.$(OBJEXT): $(top_srcdir)/internal/vm.h
@@ -16766,6 +16772,7 @@ string.$(OBJEXT): {$(VPATH)}thread.h
1676616772
string.$(OBJEXT): {$(VPATH)}thread_$(THREAD_MODEL).h
1676716773
string.$(OBJEXT): {$(VPATH)}thread_native.h
1676816774
string.$(OBJEXT): {$(VPATH)}util.h
16775+
string.$(OBJEXT): {$(VPATH)}variable.h
1676916776
string.$(OBJEXT): {$(VPATH)}vm_core.h
1677016777
string.$(OBJEXT): {$(VPATH)}vm_debug.h
1677116778
string.$(OBJEXT): {$(VPATH)}vm_opts.h
@@ -18103,6 +18110,7 @@ variable.$(OBJEXT): $(top_srcdir)/internal/serial.h
1810318110
variable.$(OBJEXT): $(top_srcdir)/internal/set_table.h
1810418111
variable.$(OBJEXT): $(top_srcdir)/internal/static_assert.h
1810518112
variable.$(OBJEXT): $(top_srcdir)/internal/string.h
18113+
variable.$(OBJEXT): $(top_srcdir)/internal/struct.h
1810618114
variable.$(OBJEXT): $(top_srcdir)/internal/symbol.h
1810718115
variable.$(OBJEXT): $(top_srcdir)/internal/thread.h
1810818116
variable.$(OBJEXT): $(top_srcdir)/internal/variable.h

gc.c

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3260,6 +3260,10 @@ rb_gc_mark_children(void *objspace, VALUE obj)
32603260
gc_mark_internal(ptr[i]);
32613261
}
32623262

3263+
if (!FL_TEST_RAW(obj, RSTRUCT_GEN_FIELDS)) {
3264+
gc_mark_internal(RSTRUCT_FIELDS_OBJ(obj));
3265+
}
3266+
32633267
break;
32643268
}
32653269

@@ -4188,6 +4192,15 @@ rb_gc_update_object_references(void *objspace, VALUE obj)
41884192
for (i = 0; i < len; i++) {
41894193
UPDATE_IF_MOVED(objspace, ptr[i]);
41904194
}
4195+
4196+
if (RSTRUCT_EMBED_LEN(obj)) {
4197+
if (!FL_TEST_RAW(obj, RSTRUCT_GEN_FIELDS)) {
4198+
UPDATE_IF_MOVED(objspace, ptr[len]);
4199+
}
4200+
}
4201+
else {
4202+
UPDATE_IF_MOVED(objspace, RSTRUCT(obj)->as.heap.fields_obj);
4203+
}
41914204
}
41924205
break;
41934206
default:

internal/struct.h

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,23 @@
1111
#include "ruby/internal/stdbool.h" /* for bool */
1212
#include "ruby/ruby.h" /* for struct RBasic */
1313

14+
/* Flags of RStruct
15+
*
16+
* 1-7: RSTRUCT_EMBED_LEN
17+
* If non-zero, the struct is embedded (its contents follow the
18+
* header, rather than being on a separately allocated buffer) and
19+
* these bits are the length of the Struct.
20+
* 8: RSTRUCT_GEN_FIELDS
21+
* The struct is embedded and has no space left to store the
22+
* IMEMO/fields reference. Any ivar this struct may have will be in
23+
* the generic_fields_tbl. This flag doesn't imply the struct has
24+
* ivars.
25+
*/
1426
enum {
1527
RSTRUCT_EMBED_LEN_MASK = RUBY_FL_USER7 | RUBY_FL_USER6 | RUBY_FL_USER5 | RUBY_FL_USER4 |
1628
RUBY_FL_USER3 | RUBY_FL_USER2 | RUBY_FL_USER1,
1729
RSTRUCT_EMBED_LEN_SHIFT = (RUBY_FL_USHIFT+1),
30+
RSTRUCT_GEN_FIELDS = RUBY_FL_USER8,
1831
};
1932

2033
struct RStruct {
@@ -23,6 +36,7 @@ struct RStruct {
2336
struct {
2437
long len;
2538
const VALUE *ptr;
39+
VALUE fields_obj;
2640
} heap;
2741
/* This is a length 1 array because:
2842
* 1. GCC has a bug that does not optimize C flexible array members
@@ -116,4 +130,31 @@ RSTRUCT_GET(VALUE st, long k)
116130
return RSTRUCT_CONST_PTR(st)[k];
117131
}
118132

133+
static inline VALUE
134+
RSTRUCT_FIELDS_OBJ(VALUE st)
135+
{
136+
const long embed_len = RSTRUCT_EMBED_LEN(st);
137+
VALUE fields_obj;
138+
if (embed_len) {
139+
RUBY_ASSERT(!FL_TEST_RAW(st, RSTRUCT_GEN_FIELDS));
140+
fields_obj = RSTRUCT_GET(st, embed_len);
141+
}
142+
else {
143+
fields_obj = RSTRUCT(st)->as.heap.fields_obj;
144+
}
145+
return fields_obj;
146+
}
147+
148+
static inline void
149+
RSTRUCT_SET_FIELDS_OBJ(VALUE st, VALUE fields_obj)
150+
{
151+
const long embed_len = RSTRUCT_EMBED_LEN(st);
152+
if (embed_len) {
153+
RUBY_ASSERT(!FL_TEST_RAW(st, RSTRUCT_GEN_FIELDS));
154+
RSTRUCT_SET(st, embed_len, fields_obj);
155+
}
156+
else {
157+
RB_OBJ_WRITE(st, &RSTRUCT(st)->as.heap.fields_obj, fields_obj);
158+
}
159+
}
119160
#endif /* INTERNAL_STRUCT_H */

struct.c

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -811,13 +811,22 @@ struct_alloc(VALUE klass)
811811
{
812812
long n = num_members(klass);
813813
size_t embedded_size = offsetof(struct RStruct, as.ary) + (sizeof(VALUE) * n);
814+
if (RCLASS_MAX_IV_COUNT(klass) > 0) {
815+
embedded_size += sizeof(VALUE);
816+
}
817+
814818
VALUE flags = T_STRUCT | (RGENGC_WB_PROTECTED_STRUCT ? FL_WB_PROTECTED : 0);
815819

816820
if (n > 0 && rb_gc_size_allocatable_p(embedded_size)) {
817821
flags |= n << RSTRUCT_EMBED_LEN_SHIFT;
818822

819823
NEWOBJ_OF(st, struct RStruct, klass, flags, embedded_size, 0);
820-
824+
if (RCLASS_MAX_IV_COUNT(klass) == 0 && embedded_size == rb_gc_obj_slot_size((VALUE)st)) {
825+
FL_SET_RAW((VALUE)st, RSTRUCT_GEN_FIELDS);
826+
}
827+
else {
828+
RSTRUCT_SET_FIELDS_OBJ((VALUE)st, 0);
829+
}
821830
rb_mem_clear((VALUE *)st->as.ary, n);
822831

823832
return (VALUE)st;

test/ruby/test_object_id.rb

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,3 +252,52 @@ def initialize
252252
end;
253253
end
254254
end
255+
256+
class TestObjectIdStruct < TestObjectId
257+
EmbeddedStruct = Struct.new(:embedded_field)
258+
259+
def setup
260+
@obj = EmbeddedStruct.new
261+
end
262+
end
263+
264+
class TestObjectIdStructGenIvar < TestObjectId
265+
GenIvarStruct = Struct.new(:a, :b, :c)
266+
267+
def setup
268+
@obj = GenIvarStruct.new
269+
end
270+
end
271+
272+
class TestObjectIdStructNotEmbed < TestObjectId
273+
MANY_IVS = 80
274+
275+
StructNotEmbed = Struct.new(*MANY_IVS.times.map { |i| :"field_#{i}" })
276+
277+
def setup
278+
@obj = StructNotEmbed.new
279+
end
280+
end
281+
282+
class TestObjectIdStructTooComplex < TestObjectId
283+
StructTooComplex = Struct.new(:a) do
284+
def initialize
285+
@too_complex_obj_id_test = 1
286+
end
287+
end
288+
289+
def setup
290+
if defined?(RubyVM::Shape::SHAPE_MAX_VARIATIONS)
291+
assert_equal 8, RubyVM::Shape::SHAPE_MAX_VARIATIONS
292+
end
293+
8.times do |i|
294+
StructTooComplex.new.instance_variable_set("@TestObjectIdStructTooComplex#{i}", 1)
295+
end
296+
@obj = StructTooComplex.new
297+
@obj.instance_variable_set("@a#{rand(10_000)}", 1)
298+
299+
if defined?(RubyVM::Shape)
300+
assert_predicate(RubyVM::Shape.of(@obj), :too_complex?)
301+
end
302+
end
303+
end

test/ruby/test_ractor.rb

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,24 @@ class TestClass
9999
RUBY
100100
end
101101

102+
def test_struct_instance_variables
103+
assert_ractor(<<~'RUBY')
104+
StructIvar = Struct.new(:member) do
105+
def initialize(*)
106+
super
107+
@ivar = "ivar"
108+
end
109+
attr_reader :ivar
110+
end
111+
obj = StructIvar.new("member")
112+
obj_copy = Ractor.new { Ractor.receive }.send(obj).value
113+
assert_equal obj.ivar, obj_copy.ivar
114+
refute_same obj.ivar, obj_copy.ivar
115+
assert_equal obj.member, obj_copy.member
116+
refute_same obj.member, obj_copy.member
117+
RUBY
118+
end
119+
102120
def test_fork_raise_isolation_error
103121
assert_ractor(<<~'RUBY')
104122
ractor = Ractor.new do

0 commit comments

Comments
 (0)