Project

General

Profile

Feature #22186

Updated by himura467 (Akito Shitara) 3 months ago

## 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`: master: 

 | 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.

Back