Issue #22186 has been updated by byroot (Jean Boussier). Here's a benchmark to demonstrate the cost of allocating larger slots, the difference being with how often GC triggers (NB: using Ruby 4.1.0dev sizes): ```ruby require 'benchmark/ips' require 'objspace' def make_size(size) klass = Class.new obj = klass.new ((size - 16) / 8).times do |i| obj.instance_variable_set("@ivar_#{i}", i) end if ObjectSpace.memsize_of(klass.new) != size raise "Failed to create size #{size}, got: #{ObjectSpace.memsize_of(klass.new)}" end klass end SIZES = [ 32, 40, 64, 128, 160, 256, 512, 640, 1024, ].to_h { |s| [s, make_size(s) ]} Benchmark.ips do |x| SIZES.each do |size, klass| x.report("#{size}") { klass.new } end x.compare!(order: :baseline) end def gc_count before = GC.count yield GC.count - before end def alloc(klass, count) count /= 10 while count > 0 count -= 1 klass.new klass.new klass.new klass.new klass.new klass.new klass.new klass.new klass.new klass.new end end puts "GC count for 10M allocations" SIZES.each do |size, klass| count = gc_count { alloc(klass, 10_000_000) } puts "size #{size}: #{count}" end ``` ``` ruby 4.1.0dev (2026-07-08T23:30:28Z master 583668d5ed) +YJIT +PRISM [arm64-darwin25] Warming up -------------------------------------- 32 3.184M i/100ms 40 2.994M i/100ms 64 3.045M i/100ms 128 2.854M i/100ms 160 2.703M i/100ms 256 2.157M i/100ms 512 1.864M i/100ms 640 1.765M i/100ms 1024 1.505M i/100ms Calculating ------------------------------------- 32 39.290M (± 2.2%) i/s (25.45 ns/i) - 197.437M in 5.025088s 40 37.197M (± 1.9%) i/s (26.88 ns/i) - 188.653M in 5.071767s 64 35.340M (± 0.5%) i/s (28.30 ns/i) - 179.672M in 5.084066s 128 32.319M (± 1.3%) i/s (30.94 ns/i) - 162.664M in 5.033113s 160 30.155M (± 0.5%) i/s (33.16 ns/i) - 151.358M in 5.019408s 256 23.283M (± 1.7%) i/s (42.95 ns/i) - 116.478M in 5.002771s 512 19.271M (± 3.6%) i/s (51.89 ns/i) - 96.903M in 5.028366s 640 17.815M (± 1.2%) i/s (56.13 ns/i) - 90.028M in 5.053602s 1024 14.912M (± 1.0%) i/s (67.06 ns/i) - 75.247M in 5.045902s Comparison: 32: 39290346.8 i/s 40: 37196774.6 i/s - 1.06x slower 64: 35340193.1 i/s - 1.11x slower 128: 32318863.3 i/s - 1.22x slower 160: 30154536.2 i/s - 1.30x slower 256: 23282729.1 i/s - 1.69x slower 512: 19271205.8 i/s - 2.04x slower 640: 17814560.6 i/s - 2.21x slower 1024: 14912457.7 i/s - 2.63x slower GC count for 10M allocations size 32: 67 size 40: 79 size 64: 133 size 128: 195 size 160: 239 size 256: 396 size 512: 781 size 640: 997 size 1024: 1604 ``` ---------------------------------------- Feature #22186: Increase the embeddable size limit for substrings created by `str_subseq()` https://bugs.ruby-lang.org/issues/22186#change-117960 * Author: himura467 (Akito Shitara) * Status: Open ---------------------------------------- ## Summary `str_subseq()` unnecessarily limits how large a sharable middle substring can be while still being embedded into the new `RString`. Substrings roughly 24 to 1000 bytes long (on a 64-bit build) always take the shared path, even though variable width allocation (VWA, Feature #18239) already supports embedding objects of that size. I propose sizing the allocation to the actual substring length via `STR_EMBEDDABLE_P()` / `str_alloc_embed()`, the same pattern used by other allocation sites in `string.c`, instead of allocating through `str_alloc_heap()` and checking whether the substring fits in the smallest heap slot class. ## Background For a sharable substring (per `SHARABLE_SUBSTRING_P`), `str_subseq()` either embeds the bytes directly into the new `RString`, or shares the parent's heap buffer, which keeps the parent alive. Since [commit 132f097149](https://github.com/ruby/ruby/commit/132f097149af36cb77308d9fe1c1a94940ab2089), the decision has been: ```c str2 = str_alloc_heap(rb_cString); if (str_embed_capa(str2) >= len + termlen) { // embed } else { // share } ``` `str_alloc_heap()` always allocates the default `sizeof(struct RString)` slot regardless of `len`, so `str_embed_capa(str2)` is capped at roughly 23 bytes. Meanwhile the default GC's size pools go up to 1024 bytes on 64-bit builds, and other allocation sites (`str_enc_new` behind `rb_str_new`, `rb_str_buf_new`, `str_new_frozen_buffer`, `rb_str_times`, ...) already pick the right-sized slot via `STR_EMBEDDABLE_P()` / `str_alloc_embed()`. ## Proposed change Draft PR: https://github.com/ruby/ruby/pull/17723 Check `STR_EMBEDDABLE_P(len, termlen)` up front and allocate with `str_alloc_embed()`; only allocate the `STR_NOEMBED` heap object on the sharing branch. Behavior is unchanged for substrings that aren't sharable or that reach the end of the parent's buffer. Only the threshold at which a sharable middle substring switches from embedding to sharing grows. ## Benchmark results Using [ruby-bench](https://github.com/ruby/ruby-bench), this branch vs current master (`arm64-darwin25`, `+PRISM`). Full results are attached as `output_001.txt`, with a per-benchmark ratio chart in `output_001-ratio-current.png`. Top 3 and bottom 3 by ratio (master/branch, higher is better): | bench | master (ms) | current (ms) | ratio | | ---- | ---- | ---- | ----- | | fluentd | 258.6 | 230.8 | 1.120 | | ruby-json | 154.1 | 150.2 | 1.026 | | liquid-c | 31.2 | 30.6 | 1.022 | | graphql-native | 172.0 | 174.9 | 0.983 | | activerecord | 148.7 | 153.0 | 0.972 | | psych-load | 1244.9 | 1291.4 | 0.964 | Note that most except fluentd varies between runs. The fluentd improvement reproduces across runs, while the entries in the bottom 3 do not. So the few 2-4% slowdowns may be noise, but I'd like to verify them with more runs and machines before treating this as risk-free. ## Relation to Feature #19315 #19315 (sharable middle substrings) addresses the same underlying problem: middle substrings copy unnecessarily. The branch benchmarked in https://bugs.ruby-lang.org/issues/19315#note-52 actually included this embed size change as one of its commits, in addition to enabling `SHARABLE_MIDDLE_SUBSTRING`. To isolate the two, I benchmarked a variant of that branch with only the embed size change reverted ([rstring-raw-ptr-reverted](https://github.com/himura467/ruby/tree/rstring-raw-ptr-reverted)), alongside this proposal, against the same master. Full results are attached as `output_002.txt`: | bench | master (ms) | this proposal (ms) | #19315 without this change (ms) | | ---- | ---- | ---- | ---- | | fluentd | 256.9 | 207.2 (x1.240) | 255.6 (x1.005) | The clear `fluentd` improvement seen in note-52 reproduces with this change alone, and disappears when this change is reverted from the #19315 branch. So the practical win observed on ruby-bench so far is attributable to the larger embed threshold rather than to `SHARABLE_MIDDLE_SUBSTRING` itself. Beyond that, this change is much narrower: - A self-contained fix to one function, reusing the VWA embedding infrastructure already battle-tested elsewhere in `string.c`. - No API/ABI implications; `RSTRING_PTR()`'s `\0`-termination guarantee is untouched. - No memory-retention risk, since embedding copies the bytes instead of referencing the parent. The two are orthogonal (#19315 still helps for slices too large to embed, as its micro-benchmarks show), but this smaller fix seems worth landing first on its own merits. ---Files-------------------------------- output_001.txt (7.76 KB) output_001-ratio-current.png (184 KB) output_002.txt (14.9 KB) string_subseq.yml (418 Bytes) str_subseq_memory.rb (1.25 KB) -- https://bugs.ruby-lang.org/