diff --git a/array.c b/array.c index 26ff60fb79738c..a05d57b83105fa 100644 --- a/array.c +++ b/array.c @@ -66,6 +66,10 @@ VALUE rb_cArray_empty_frozen; * 14: RARRAY_PTR_IN_USE_FLAG * The buffer of the array is in use. This is only used during * debugging. + * 19: RARRAY_FAKEARY + * The array is not allocated or managed by the garbage collector. + * Typically, the array object header (struct RString) is temporarily + * allocated on C stack. */ /* for OPTIMIZED_CMP: */ @@ -188,7 +192,7 @@ ARY_SET(VALUE a, long i, VALUE v) static long ary_embed_capa(VALUE ary) { - size_t size = rb_gc_obj_slot_size(ary) - offsetof(struct RArray, as.ary); + size_t size = rb_obj_shape_slot_size(ary) - offsetof(struct RArray, as.ary); RUBY_ASSERT(size % sizeof(VALUE) == 0); return size / sizeof(VALUE); } @@ -900,7 +904,7 @@ static VALUE init_fake_ary_flags(void) { struct RArray fake_ary = {0}; - fake_ary.basic.flags = T_ARRAY; + fake_ary.basic.flags = T_ARRAY | RARRAY_FAKEARY; VALUE ary = (VALUE)&fake_ary; RBASIC_SET_FULL_SHAPE_ID(ary, ROOT_SHAPE_ID | SHAPE_ID_LAYOUT_OTHER); rb_ary_freeze(ary); @@ -2380,6 +2384,24 @@ rb_ary_set_len(VALUE ary, long len) ARY_SET_LEN(ary, len); } +VALUE +rb_ary_modify_expand(VALUE ary, long expand) +{ + long len = RARRAY_LEN(ary); + + if (expand < 0) { + rb_raise(rb_eArgError, "negative expanding array size"); + } + if (expand >= ARY_MAX_SIZE - len) { + rb_raise(rb_eArgError, " size too big"); + } + rb_ary_modify_check(ary); + if (len + expand > ARY_CAPA(ary)) { + ary_resize_capa(ary, len + expand); + } + return ary; +} + VALUE rb_ary_resize(VALUE ary, long len) { diff --git a/benchmark/string_concat.yml b/benchmark/string_concat.yml index c07fd21013f4d0..e21b6e5506b948 100644 --- a/benchmark/string_concat.yml +++ b/benchmark/string_concat.yml @@ -1,10 +1,18 @@ prelude: | + # frozen_string_literal: true CHUNK = "a" * 64 UCHUNK = "é" * 32 SHORT = "a" * (GC.stat_heap(0, :slot_size) / 2) LONG = "a" * (GC.stat_heap(0, :slot_size) * 2) GC.disable # GC causes a lot of variance benchmark: + binary_concat_embedded: | + # Concat 20 times in a String with 23 capacity + # Excercise `str_embed_capa` + buffer = +"" + buffer << "1" << "2" << "3" << "4" << "5" << "6" << "7" << "8" << "9" << "0" + buffer << "1" << "2" << "3" << "4" << "5" << "6" << "7" << "8" << "9" << "0" + binary_concat_7bit: | buffer = String.new(capacity: 4096, encoding: Encoding::BINARY) buffer << CHUNK << CHUNK << CHUNK << CHUNK << CHUNK << CHUNK << CHUNK << CHUNK diff --git a/bignum.c b/bignum.c index 28924b4eb9cd09..088e8c49b55024 100644 --- a/bignum.c +++ b/bignum.c @@ -3002,7 +3002,7 @@ rb_cmpint(VALUE val, VALUE a, VALUE b) static size_t big_embed_capa(VALUE big) { - size_t size = rb_gc_obj_slot_size(big) - offsetof(struct RBignum, as.ary); + size_t size = rb_obj_shape_slot_size(big) - offsetof(struct RBignum, as.ary); RUBY_ASSERT(size % sizeof(BDIGIT) == 0); size_t capa = size / sizeof(BDIGIT); RUBY_ASSERT(capa <= BIGNUM_EMBED_LEN_MAX); diff --git a/gc.c b/gc.c index aec4aba173a9f8..d12e37f4346bed 100644 --- a/gc.c +++ b/gc.c @@ -376,8 +376,6 @@ rb_gc_shutdown_call_finalizer_p(VALUE obj) void rb_gc_obj_changed_slot_size(VALUE obj, size_t slot_size) { - RUBY_ASSERT(RB_TYPE_P(obj, T_OBJECT)); - RBASIC_SET_FULL_SHAPE_ID(obj, rb_obj_shape_transition_slot_size(obj, slot_size)); } @@ -5577,14 +5575,24 @@ ruby_xrealloc2(void *ptr, size_t n, size_t size) #undef ruby_xfree_sized #endif +/* + * This is a debugging flag for measuring the cost of `xfree`. + * It can be enabled at compile time using `-DRUBY_NO_FREE`. + * At run time, if the `RUBY_NO_FREE` environment variable is set to "1", + * then `xfree` will not free any memory. + */ +#ifdef RUBY_NO_FREE static bool g_nofree = false; +#endif void ruby_xfree_sized(void *x, size_t size) { +#ifdef RUBY_NO_FREE if (g_nofree) { return; } +#endif if (RUBY_DTRACE_GC_XFREE_ENABLED()) { RUBY_DTRACE_GC_XFREE(x, size); @@ -5853,11 +5861,13 @@ rb_gc_checking_shareable(void) void Init_GC(void) { +#ifdef RUBY_NO_FREE const char* nofree_str = getenv("RUBY_NO_FREE"); - if (nofree_str && strcmp(nofree_str, "yes") == 0) { + if (nofree_str && strcmp(nofree_str, "1") == 0) { fprintf(stderr, "WARNING: Enabling no-free mode! xfree() will never free anything!\n"); g_nofree = true; } +#endif #undef rb_intern rb_gc_register_address(&id2ref_value); diff --git a/gc/default/default.c b/gc/default/default.c index 5cc14e22ddfae2..431d039e205439 100644 --- a/gc/default/default.c +++ b/gc/default/default.c @@ -7616,7 +7616,7 @@ gc_move(rb_objspace_t *objspace, VALUE src, VALUE dest, struct heap_page *src_pa /* Move the object */ memcpy((void *)dest, (void *)src, MIN(src_slot_size, slot_size)); - if (src_slot_size != slot_size && RB_TYPE_P(src, T_OBJECT)) { + if (src_slot_size != slot_size) { rb_gc_obj_changed_slot_size(dest, slot_size - RVALUE_OVERHEAD); } diff --git a/internal/array.h b/internal/array.h index a2cd06f2e3f34a..c7085f431fa31e 100644 --- a/internal/array.h +++ b/internal/array.h @@ -21,6 +21,7 @@ #define RARRAY_SHARED_FLAG ELTS_SHARED #define RARRAY_SHARED_ROOT_FLAG FL_USER12 #define RARRAY_PTR_IN_USE_FLAG FL_USER14 +#define RARRAY_FAKEARY FL_USER19 /* array.c */ VALUE rb_ary_hash_values(long len, const VALUE *elements); @@ -38,6 +39,7 @@ void rb_ary_make_embedded(VALUE ary); bool rb_ary_embeddable_p(VALUE ary); VALUE rb_ary_diff(VALUE ary1, VALUE ary2); VALUE rb_ary_compact_bang(VALUE ary); +VALUE rb_ary_modify_expand(VALUE ary, long expand); RUBY_EXTERN VALUE rb_cArray_empty_frozen; static inline VALUE rb_ary_entry_internal(VALUE ary, long offset); diff --git a/internal/string.h b/internal/string.h index 94a46a96573322..02d20a462627f8 100644 --- a/internal/string.h +++ b/internal/string.h @@ -21,6 +21,7 @@ #define STR_CHILLED (FL_USER2 | FL_USER3) #define STR_CHILLED_LITERAL FL_USER2 #define STR_CHILLED_SYMBOL_TO_S FL_USER3 +#define STR_FAKESTR FL_USER19 enum ruby_rstring_private_flags { RSTRING_CHILLED = STR_CHILLED, diff --git a/pack.c b/pack.c index b05bc88caead7a..378c5e17a2315f 100644 --- a/pack.c +++ b/pack.c @@ -962,6 +962,9 @@ hex2num(char c) #define PACK_LENGTH_ADJUST_SIZE(sz) do { \ tmp_len = 0; \ + if (mode == UNPACK_ARRAY) { \ + rb_ary_modify_expand(ary, len); \ + } \ if (len > (long)((send-s)/(sz))) { \ if (!star) { \ tmp_len = len-(send-s)/(sz); \ @@ -972,7 +975,7 @@ hex2num(char c) #define PACK_ITEM_ADJUST() do { \ if (tmp_len > 0 && mode == UNPACK_ARRAY) \ - rb_ary_store(ary, RARRAY_LEN(ary)+tmp_len-1, Qnil); \ + rb_ary_resize(ary, RARRAY_LEN(ary)+tmp_len); \ } while (0) /* Workaround for Oracle Developer Studio (Oracle Solaris Studio) @@ -992,7 +995,7 @@ enum unpack_mode { }; static VALUE -pack_unpack_internal(VALUE str, VALUE fmt, enum unpack_mode mode, long offset) +pack_unpack_internal(VALUE str, VALUE fmt, VALUE ofs, enum unpack_mode mode) { #define hexdigits ruby_hexdigits const char *s, *send; @@ -1016,6 +1019,7 @@ pack_unpack_internal(VALUE str, VALUE fmt, enum unpack_mode mode, long offset) StringValue(str); StringValue(fmt); + long offset = NUM2LONG(ofs); rb_must_asciicompat(fmt); len = RSTRING_LEN(str); @@ -1672,13 +1676,13 @@ static VALUE pack_unpack(rb_execution_context_t *ec, VALUE str, VALUE fmt, VALUE offset) { enum unpack_mode mode = rb_block_given_p() ? UNPACK_BLOCK : UNPACK_ARRAY; - return pack_unpack_internal(str, fmt, mode, RB_NUM2LONG(offset)); + return pack_unpack_internal(str, fmt, offset, mode); } static VALUE pack_unpack1(rb_execution_context_t *ec, VALUE str, VALUE fmt, VALUE offset) { - return pack_unpack_internal(str, fmt, UNPACK_1, RB_NUM2LONG(offset)); + return pack_unpack_internal(str, fmt, offset, UNPACK_1); } int diff --git a/pathname_builtin.rb b/pathname_builtin.rb index ade839e2bc94bf..f2dbee5a7cf7df 100644 --- a/pathname_builtin.rb +++ b/pathname_builtin.rb @@ -1889,7 +1889,22 @@ def directory?() FileTest.directory?(@path) end # See FileTest.file?. def file?() FileTest.file?(@path) end - # See FileTest.pipe?. + # :markup: markdown + # + # call-seq: + # pipe? -> true or false + # + # Returns whether the path in +self+ points to a pipe: + # + # ```ruby + # path = '/tmp/foo' + # File.mkfifo(path) + # pn = Pathname(path) # => # + # pn.pipe? # => true + # Pathname('.').pipe? # => false + # pn.delete # Clean up. + # ``` + # def pipe?() FileTest.pipe?(@path) end # See FileTest.socket?. @@ -1917,13 +1932,37 @@ def socket?() FileTest.socket?(@path) end # def owned?() FileTest.owned?(@path) end - # See FileTest.readable?. + # :markup: markdown + # + # call-seq: + # readable? -> true or false + # + # Returns whether the path in `self` points to an entry + # that is readable by the owner and group of the current process: + # + # ```ruby + # pn = Pathname('/tmp/secret.txt') + # pn.write('foo') + # pn.readable? # => true + # pn.chmod(0o000) + # pn.readable? # => false + # pn.delete # Clean up. + # Pathname('nosuch').readable? # => false + # ``` + # def readable?() FileTest.readable?(@path) end # See FileTest.world_readable?. def world_readable?() File.world_readable?(@path) end - # See FileTest.readable_real?. + # :markup: markdown + # + # call-seq: + # + # readable_real? -> true or false + # + # Like #readable?, but checks against the real user and group ids + # instead of the effective ids. def readable_real?() FileTest.readable_real?(@path) end # See FileTest.setuid?. diff --git a/ractor.c b/ractor.c index 58677a0b470dae..cb603f40b93389 100644 --- a/ractor.c +++ b/ractor.c @@ -2032,7 +2032,7 @@ move_enter(VALUE obj, struct obj_traverse_replace_data *data) } else { VALUE type = RB_BUILTIN_TYPE(obj); - size_t slot_size = rb_gc_obj_slot_size(obj); + size_t slot_size = rb_obj_shape_slot_size(obj); VALUE moved = rb_newobj(GET_EC(), 0, type, RBASIC_SHAPE_ID(obj), wb_protected_types[type], slot_size); MEMZERO(((struct RBasic *)moved) + 1, char, slot_size - sizeof(struct RBasic)); data->replacement = (VALUE)moved; @@ -2050,7 +2050,7 @@ move_leave(VALUE obj, struct obj_traverse_replace_data *data) memcpy( (char *)data->replacement + sizeof(VALUE), (char *)obj + sizeof(VALUE), - rb_gc_obj_slot_size(obj) - sizeof(VALUE) + rb_obj_shape_slot_size(obj) - sizeof(VALUE) ); // We've copied obj's references to the replacement diff --git a/set.c b/set.c index 9a9a7636f87367..e5853e52b44a93 100644 --- a/set.c +++ b/set.c @@ -2311,44 +2311,27 @@ rb_set_size(VALUE set) /* * Document-class: Set * - * The Set class implements a collection of unordered values with no - * duplicates. It is a hybrid of Array's intuitive inter-operation - * facilities and Hash's fast lookup. - * - * Set is easy to use with Enumerable objects (implementing #each). - * Most of the initializer methods and binary operators accept generic - * Enumerable objects besides sets and arrays. An Enumerable object - * can be converted to Set using the +to_set+ method. - * - * Set uses a data structure similar to Hash for storage, except that - * it only has keys and no values. - * - * * Equality of elements is determined according to Object#eql? and - * Object#hash. Use Set#compare_by_identity to make a set compare - * its elements by their identity. - * * Set assumes that the identity of each element does not change - * while it is stored. Modifying an element of a set will render the - * set to an unreliable state. - * * When a string is to be stored, a frozen copy of the string is - * stored instead unless the original string is already frozen. - * - * == Comparison - * - * The comparison operators <, >, <=, and - * >= are implemented as shorthand for the - * {proper_,}{subset?,superset?} methods. The <=> - * operator reflects this order, or returns +nil+ for sets that both - * have distinct elements ({x, y} vs. {x, z} for example). - * - * == Example - * - * s1 = Set[1, 2] #=> Set[1, 2] - * s2 = [1, 2].to_set #=> Set[1, 2] - * s1 == s2 #=> true - * s1.add("foo") #=> Set[1, 2, "foo"] - * s1.merge([2, 6]) #=> Set[1, 2, "foo", 6] - * s1.subset?(s2) #=> false - * s2.subset?(s1) #=> true + * An instance of class \Set contains a collection + * of objects (elements), with no duplicates. + * + * By default: + * + * - Set determines equality via Object#eql? and Object#hash, + * and assumes that these values do not change for a stored element. + * If these values do change, the set enters an unreliable state; + * see #reset. + * - A String instance added to a set is stored as a frozen copy of the string, + * unless it is already frozen. + * + * Calling #compare_by_identity causes: + * + * - All following determinations of equality + * to use object identity instead of the methods mentioned above. + * - A String added to a set is stored "as is", whether or not frozen. + * + * \Set includes module Enumerable, and is easy to use with other enumerable objects. + * Many of its methods accept enumerable objects as arguments; + * any enumerable object may be converted to a set via #to_set. * * == Contact * @@ -2356,35 +2339,30 @@ rb_set_size(VALUE set) * * == Inheriting from \Set * - * Before Ruby 4.0 (released December 2025), \Set had a different, less - * efficient implementation. It was reimplemented in C, and the behavior - * of some of the core methods were adjusted. - * - * To keep backward compatibility, when a class is inherited from \Set, - * additional module +Set::SubclassCompatible+ is included, which makes - * the inherited class behavior, as well as internal method names, - * closer to what it was before Ruby 4.0. + * Before Ruby 4.0 (released in December, 2025), + * class \Set had a different, less efficient implementation. + * In Ruby 4.0, the class was reimplemented in C, + * and the behaviors of some methods were adjusted. * - * It can be easily seen, for example, in the #inspect method behavior: + * When compatibility with the older implementation is needed, + * a \Set subclass should inherit directly from class +Set+; + * this automatically includes module +Set::SubclassCompatible+, + * which makes behaviors closer to those in the older implementation. * - * p Set[1, 2, 3] - * # prints "Set[1, 2, 3]" + * A difference may be seen as follows: * - * class MySet < Set - * end - * p MySet[1, 2, 3] - * # prints "#", like it was in Ruby 3.4 + * Set[[1, 2, 3]] # => Set[[1, 2, 3]] + * class MySet < Set; end + * MySet[[1, 2, 3]] # => # # Same as in Ruby 3.4. * - * For new code, if backward compatibility is not necessary, - * it is recommended to instead inherit from +Set::CoreSet+, which - * avoids including the "compatibility" layer: + * When backward compatibility is not needed, + * a \Set subclass should inherit from +Set::CoreSet+, + * which avoids including the compatibility layer: * - * class MyCoreSet < Set::CoreSet - * end - * p MyCoreSet[1, 2, 3] - * # prints "MyCoreSet[1, 2, 3]" + * class MyCoreSet < Set::CoreSet; end + * MyCoreSet[[1, 2, 3]] # => MyCoreSet[[1, 2, 3]] * - * == Set's methods + * == What's Here * * First, what's elsewhere. \Class \Set: * diff --git a/shape.c b/shape.c index 7e48654c7c5c96..7bbf1e0328d380 100644 --- a/shape.c +++ b/shape.c @@ -1243,6 +1243,21 @@ rb_shape_expected_layout(VALUE obj) } } +bool +rb_shape_verify_capacity_consistency_p(VALUE obj) +{ + switch (BUILTIN_TYPE(obj)) { + case T_IMEMO: + return IMEMO_TYPE_P(obj, imemo_fields); + case T_STRING: + return !FL_TEST_RAW(obj, FL_USER19); // STR_FAKESTR + case T_ARRAY: + return !FL_TEST_RAW(obj, RARRAY_FAKEARY); + default: + return true; + } +} + bool rb_shape_verify_consistency(VALUE obj, shape_id_t shape_id) { @@ -1305,8 +1320,8 @@ rb_shape_verify_consistency(VALUE obj, shape_id_t shape_id) } } - attr_index_t shape_id_capacity = rb_shape_embedded_capacity(shape_id); - if (RB_TYPE_P(obj, T_OBJECT) || IMEMO_TYPE_P(obj, imemo_fields)) { + if (rb_shape_verify_capacity_consistency_p(obj)) { + attr_index_t shape_id_capacity = rb_shape_embedded_capacity(shape_id); RUBY_ASSERT(shape_id_capacity > 0); size_t shape_id_slot_size = shape_id_capacity * sizeof(VALUE) + sizeof(struct RBasic); diff --git a/shape.h b/shape.h index fb5a9616e0ca13..86f98b670782fb 100644 --- a/shape.h +++ b/shape.h @@ -286,6 +286,19 @@ rb_shape_embedded_capacity(shape_id_t shape_id) return (attr_index_t)((shape_id & SHAPE_ID_CAPACITY_MASK) >> SHAPE_ID_CAPACITY_OFFSET); } +static inline size_t +rb_shape_slot_size(shape_id_t shape_id) +{ + return sizeof(struct RBasic) + (rb_shape_embedded_capacity(shape_id) * sizeof(VALUE)); +} + +static inline size_t +rb_obj_shape_slot_size(VALUE obj) +{ + RUBY_ASSERT(!RB_TYPE_P(obj, T_IMEMO) || IMEMO_TYPE_P(obj, imemo_fields)); + return rb_shape_slot_size(RBASIC_SHAPE_ID(obj)); +} + static inline attr_index_t rb_shape_capacity_for_slot_size(size_t slot_size) { diff --git a/string.c b/string.c index 73e16b031d3a9b..8d82cdee823b6a 100644 --- a/string.c +++ b/string.c @@ -131,7 +131,6 @@ VALUE rb_cSymbol; #define STR_BORROWED FL_USER6 #define STR_TMPLOCK FL_USER7 #define STR_NOFREE FL_USER18 -#define STR_FAKESTR FL_USER19 #define STR_SET_NOEMBED(str) do {\ FL_SET((str), STR_NOEMBED);\ @@ -223,7 +222,7 @@ SHARABLE_SUBSTRING_P(VALUE str, long beg, long len) static inline long str_embed_capa(VALUE str) { - return rb_gc_obj_slot_size(str) - offsetof(struct RString, as.embed.ary); + return rb_obj_shape_slot_size(str) - offsetof(struct RString, as.embed.ary); } bool diff --git a/struct.c b/struct.c index 59ac221b681a0e..72a5fa7e3d22e9 100644 --- a/struct.c +++ b/struct.c @@ -840,7 +840,7 @@ struct_alloc(VALUE klass) NEWOBJ_OF(st, struct RStruct, klass, flags, embedded_size); if (RCLASS_MAX_IV_COUNT(klass) == 0) { if (!rb_obj_shape_has_fields((VALUE)st) - && embedded_size < rb_gc_obj_slot_size((VALUE)st)) { + && embedded_size < rb_obj_shape_slot_size((VALUE)st)) { FL_UNSET_RAW((VALUE)st, RSTRUCT_GEN_FIELDS); RSTRUCT_SET_FIELDS_OBJ((VALUE)st, 0); } diff --git a/test/objspace/test_objspace.rb b/test/objspace/test_objspace.rb index ba051b670f8616..84cd88659ee2f0 100644 --- a/test/objspace/test_objspace.rb +++ b/test/objspace/test_objspace.rb @@ -722,7 +722,7 @@ def test_dump_includes_slot_size obj = klass.new dump = ObjectSpace.dump(obj) - assert_includes dump, "\"slot_size\":#{GC.stat_heap(0, :slot_size) - GC::INTERNAL_CONSTANTS[:RVALUE_OVERHEAD]}" + assert_match /"slot_size":\d+/, dump end def test_dump_reference_addresses_match_dump_all_addresses diff --git a/test/ruby/test_time.rb b/test/ruby/test_time.rb index b2cbd06a9f3ab5..91bf4f763b1233 100644 --- a/test/ruby/test_time.rb +++ b/test/ruby/test_time.rb @@ -1434,9 +1434,12 @@ def test_memsize end sizeof_vtm = RbConfig::SIZEOF["void*"] * 4 + 8 data_size = GC::INTERNAL_CONSTANTS[:RVALUE_SIZE] + sizeof_timew + sizeof_vtm - # Round up to the smallest slot size that fits - slot_sizes = GC::INTERNAL_CONSTANTS[:HEAP_COUNT].times.map { |i| GC.stat_heap(i, :slot_size) } - expect = slot_sizes.find { |s| s >= data_size } || slot_sizes.last + expect = data_size + if GC::INTERNAL_CONSTANTS[:HEAP_COUNT] + # Round up to the smallest slot size that fits + slot_sizes = GC::INTERNAL_CONSTANTS[:HEAP_COUNT].times.map { |i| GC.stat_heap(i, :slot_size) } + expect = slot_sizes.find { |s| s >= data_size } || slot_sizes.last + end assert_operator ObjectSpace.memsize_of(t), :<=, expect rescue LoadError => e omit "failed to load objspace: #{e.message}"