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}"