Skip to content

Optimize method chains by updating intermediate objects - #17685

Draft
mame wants to merge 9 commits into
ruby:masterfrom
mame:opt-fresh-flag
Draft

Optimize method chains by updating intermediate objects#17685
mame wants to merge 9 commits into
ruby:masterfrom
mame:opt-fresh-flag

Conversation

@mame

@mame mame commented Jul 6, 2026

Copy link
Copy Markdown
Member

This is an experimental optimization that speeds up typical method chains by destructively updating their intermediate objects:

  • ary.map {}.select {} runs like ary.map {}.select! {}
  • str.strip.upcase runs like str.strip.upcase!
  • "foo" + str + str runs like "foo" << str << str

Intermediate String/Array/Hash objects whose only reference is a single value-stack slot are tagged with a FRESH flag, and the next built-in call in the chain reuses them in place. Whether a call is optimized is decided via the method cache: both an advisory callinfo bit and a flag on the built-in's method entry must be set, so redefined methods naturally opt out.

Common idioms get 1.05-1.5x faster ("foo" + str 1.5x, ary + ary + ary 2.1x, ary.uniq.sort.reverse 1.06x), and non-optimized code is unaffected.

Still a PoC. All of btest and the relevant test-all suites pass.

mame added 4 commits July 7, 2026 02:50
A flag asserting the only reference to the object is a single
value-stack slot, so an optimized consumer may reuse it destructively.
ObjectSpace.each_object unmarks before yielding.
Preparatory refactoring: split out the buffer allocation part so that
later changes can allocate a string with a capacity larger than its
initial length.  No behavioral change.
prism marks + sends whose result feeds directly into another + as
opt_plus_fresh: the producer allocates with growth headroom and tags
the result STR_FRESH, and the existing opt_plus appends in place when
its receiver carries the tag.  A literal head is duplicated by
dupstring_fresh, which requests twice the length so the size-pool
rounding leaves headroom; too-long literals fall back to the plain
sharing duplication, unarmed.
Same scheme as String: opt_plus_fresh allocates the chain head with
growth headroom and marks it ARY_FRESH, the existing opt_plus
concatenates onto a marked receiver in place, and the chain end
restores the exact-capacity result.
@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown

The following files are maintained in the following upstream repositories:

Please file a pull request to the above instead. Thank you!

mame added 5 commits July 7, 2026 03:13
prism marks every send whose result directly becomes the receiver of
another send with advisory callinfo bits (VM_CALL_FRESH_PROD/CONS).
Whether anything happens is decided when the call cache is filled:
both the callinfo bit and the fresh_producer/fresh_consumer bit on
the method entry must be set.  Marked builtins arm STR_FRESH on
their result, and consumers call the bang counterpart on a receiver
that carries it.  A consumer resolving anywhere else drops the flag
before the receiver can escape.
Array consumers get flat fresh implementations that skip the bangs'
modify/enumerator/repair overhead.  Fresh-array producers join the
table: literals, String#split/chars/lines, Range#to_a and
Enumerable#flat_map/sort_by.  Under a JIT, array.rb replaces
map/select with Ruby definitions, which trips the kill switch at
boot: Array chains are then simply left alone.
merge/transform_values/select/reject/compact consume a fresh receiver
in place; Hash#invert joins as a producer.
f(*xs.map { }) and foo(**opts.merge(defaults)) mark the producer; the
callee-side rest/kwrest setup takes the fresh operand as *_SPLAT_MUT,
eliding the defensive dup and never leaking the mark.  This is the
runtime generalization of VM_CALL_ARGS_SPLAT_MUT / KW_SPLAT_MUT.
The _fresh producer insns get arming twins of their plain gen
functions, and + sites marked as chain members substitute the
consume/arm entry points.  Marked builtin sends just call the plain
cfunc in JITed code: sound but inactive (the interpreter does the
in-place reuse).  ZJIT side-exits on the _fresh insns.
@mame
mame force-pushed the opt-fresh-flag branch from a844e92 to 429317b Compare July 6, 2026 18:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant