Prevent ObjectSpace.count_objects from allocating extra arrays

`ObjectSpace.count_objects` could cause an unintended array allocation.
It returns a hash like `{ :T_ARRAY => 100, :T_STRING => 100, ... }`, so
it creates the key symbol (e.g., `:T_STRING`) for the first time. On
rare occations, this symbol creation internally allocates a new array
for symbol management.

This led to a problematic side effect where calling `count_objects`
twice in a row could produce inconsistent results: the first call would
trigger the hidden array allocation, and the second call would then
report an increased count for `:T_ARRAY`.

This behavior caused test failures in `test/ruby/test_allocation.rb`,
which performs a baseline measurement before an operation and then
asserts the exact number of new allocations.

20250716T053005Z.fail.html.gz

>   1) Failure:
> TestAllocation::ProcCall::WithBlock#test_ruby2_keywords [...]:
> Expected 1 array allocations for "r2k.(1, a: 2, &block)", but 2 arrays allocated.

This change resolves the issue by pre-interning all key symbols used by
`ObjectSpace.count_objects` before its counting. This eliminates the
side effect and ensures the stability of allocation-sensitive tests.

Co-authored-by: Koichi Sasada <ko1@atdot.net>
This commit is contained in:
Yusuke Endoh 2025-07-16 17:20:21 +09:00
parent 3956308aa4
commit 6d17a3e647

11
gc.c
View file

@ -2498,6 +2498,7 @@ count_objects(int argc, VALUE *argv, VALUE os)
{ {
struct count_objects_data data = { 0 }; struct count_objects_data data = { 0 };
VALUE hash = Qnil; VALUE hash = Qnil;
VALUE types[T_MASK + 1];
if (rb_check_arity(argc, 0, 1) == 1) { if (rb_check_arity(argc, 0, 1) == 1) {
hash = argv[0]; hash = argv[0];
@ -2505,6 +2506,10 @@ count_objects(int argc, VALUE *argv, VALUE os)
rb_raise(rb_eTypeError, "non-hash given"); rb_raise(rb_eTypeError, "non-hash given");
} }
for (size_t i = 0; i <= T_MASK; i++) {
types[i] = type_sym(i);
}
rb_gc_impl_each_object(rb_gc_get_objspace(), count_objects_i, &data); rb_gc_impl_each_object(rb_gc_get_objspace(), count_objects_i, &data);
if (NIL_P(hash)) { if (NIL_P(hash)) {
@ -2517,9 +2522,9 @@ count_objects(int argc, VALUE *argv, VALUE os)
rb_hash_aset(hash, ID2SYM(rb_intern("FREE")), SIZET2NUM(data.freed)); rb_hash_aset(hash, ID2SYM(rb_intern("FREE")), SIZET2NUM(data.freed));
for (size_t i = 0; i <= T_MASK; i++) { for (size_t i = 0; i <= T_MASK; i++) {
VALUE type = type_sym(i); if (data.counts[i]) {
if (data.counts[i]) rb_hash_aset(hash, types[i], SIZET2NUM(data.counts[i]));
rb_hash_aset(hash, type, SIZET2NUM(data.counts[i])); }
} }
return hash; return hash;