vm.c: answer str[0] from C in OP_GETIDX0 - #7040
Merged
Merged
Conversation
`vm_op_getidx0()` has branches for Array and Hash but none for String, so `str[0]` is the one index form of the three that leaves the opcode through `getidx0_fallback` and reaches `String#[]` as an ordinary send. Its sibling `vm_op_getidx()` does have a String branch, so `str[1]` is already answered in C. The two opcodes disagree about String for no reason other than the branch never having been written. `while i < n; s[0]; i += 1; end` takes 35 ms at `n` = 800000 and 137 ms at 3200000, against 21 ms and 87 ms with the branch. Both are linear, so this is a constant factor rather than a complexity change. `mrb_str_aref()` allocates, and an inline opcode never runs the cfunc epilogue that would shrink the arena, so the branch saves and restores it the way the Hash branch beside it does. That is the bug the allocating inline opcodes were fixed for; a String branch written without it would re-open the same hole one index form later, and under `MRB_GC_FIXED_ARENA`, which `build_config/ci` defines, the loop above raises `NoMemoryError: arena overflow error` instead of merely going quadratic. Unlike the Hash branch this one does not refresh `ci`, since `mrb_str_aref()` on a plain String with an Integer index cannot run Ruby code and so cannot move the stack. The result needs no `mrb_gc_protect()` either: it is stored in `regs[a]`, and the VM stack is a GC root. The class guard reads the receiver's class rather than whether `String#[]` has been redefined, so the branch bypasses a redefinition installed on `String` itself. That is observable: ```ruby class String def [](i); "OVERRIDDEN"; end end s = "hello" s[0] # => "OVERRIDDEN" before, "h" now s[1] # => "e" before and now ``` The change is to make `s[0]` agree with `s[1]`, which has ignored such a redefinition ever since `vm_op_getidx()` gained its String branch. A subclass receiver fails the guard and still reaches the override. The Array and Hash branches of both opcodes work the same way, so this does not widen the behaviour beyond one more index form. The assertion in `test/t/gc.rb` is a second one for `OP_GETIDX0`, naming the branch it pins so it does not read as the existing Hash one moved; it fails if this branch is added without the arena restore. The one in `test/t/string.rb` pins the redefinition behaviour, which nothing in the suite covered, for both index forms at once.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (3)
📝 WalkthroughWalkthrough
ChangesString index-zero fast path
Estimated code review effort: 3 (Moderate) | ~20 minutes Possibly related PRs
Suggested labels: Suggested reviewers: 🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
This was referenced Aug 9, 2026
This was referenced Aug 16, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The gap
vm_op_getidx0()answersary[0]andhash[0]from C and has no Stringbranch, so
str[0]is the one index form of the three that leaves the opcodethrough
getidx0_fallbackand reachesString#[]as an ordinary send. Itssibling
vm_op_getidx()does have a String branch, sostr[1]is alreadyanswered in C. The two opcodes disagree about String for no reason other than
the branch never having been written.
What it costs
nstr[0]beforeBest of seven runs each, alternating the two binaries, with
while i < n; s[0]; i += 1; endover a local. Both are linear, so this is aconstant factor rather than a complexity change.
The arena restore is not optional
mrb_str_aref()returns a freshly allocated String, and an inline opcode neverruns the cfunc epilogue that would shrink the arena. That is the bug #7022
fixed across the allocating inline opcodes, including the Hash branch of this
same
vm_op_getidx0(). A String branch written without the save and restorewould re-open it one index form later: the loop above becomes quadratic, and
under
MRB_GC_FIXED_ARENA, whichbuild_config/ci/gcc-clang.rbandbuild_config/ci/msvc.rbboth define, it raises instead:With the restore in place the same command prints nothing and exits 0.
Unlike the Hash branch above it, this one does not refresh
ciafterwards:mrb_str_aref()on a plain String with an Integer index cannot run Ruby codeand so cannot move the stack. The result needs no
mrb_gc_protect()either,since it is stored in
regs[a]and the VM stack is a GC root, which is why thecfunc epilogues can shrink unconditionally too.
It changes what a redefined
String#[]seesThe class guard reads the receiver's class, not whether
String#[]has beenredefined, so the branch bypasses a redefinition installed on
Stringitself:The change is to make
s[0]agree withs[1], which has ignored such aredefinition ever since
vm_op_getidx()gained its String branch. A subclassreceiver keeps reaching the override, because the guard rejects it. Whether the
existing
s[1]behaviour is right is a separate and much larger question,since the Array and Hash branches of both opcodes work the same way; this
change does not widen it beyond one more index form.
Which shapes reach the opcode
OP_GETIDX0is emitted for a literal zero index only when the receiver isalready in a register: a local, a method argument, a block-local. These are
OP_GETIDX0, and slow before this change:and these are not, because the receiver has to be materialized first, after
which the compiler emits the general
OP_GETIDXwith aLOADI_0beside it andthe String branch of that opcode answers it in C:
A benchmark that puts the loop inside a block over an outer
sthereforemeasures the C path and shows no difference at all. The figures above all come
from a
whileloop over a local.The tests
test/t/gc.rbalready carries anOP_GETIDX0arena assertion for the Hashbranch, added by #7022. The new one is a second assertion for the same opcode,
naming the branch it pins so the two do not read as one assertion moved. It
passes without this change, since there is nothing to leak without a String
branch, and is there to fail if the branch is ever added or rewritten without
the restore. Checked both ways: it passes with the branch as written, and fails
with the same branch minus the two arena calls.
test/t/string.rbgets an assertion for the redefinition case above, whichnothing in the suite covered, and it pins both index forms at once. It restores
String#[]in anensure. The saved alias is removed only whereremove_methodexists, since that comes from mruby-metaprog and the core testbuild does not have it; for the same reason the assertion does not use
send.Checked configurations
All at
mruby/mruby@9df343588, with the change applied.build_config/default.rb)full-debug(MRB_GC_STRESS,MRB_USE_DEBUG_HOOK)MRB_GC_FIXED_ARENA+ bintestcxx_abiSummary by CodeRabbit
Performance
Bug Fixes
Tests