Remove leading nop from block when we don't need it

Blocks insert a leading `nop` instruction in order to execute a "block
call" tracepoint. Block compilation unconditionally inserts a leading
`nop` plus a label after the instruction:

  641f15b1c6/prism_compile.c (L6867-L6869)

This `nop` instruction is used entirely for firing the block entry
tracepoint.  The label exists so that the block can contain a loop but
the block entry tracepoint is executed only once.

For example, the following code is an infinite loop, but should only
execute the b_call tracepoint once:

```ruby
-> { redo }.call
```

Previous to this commit, we would eliminate the `nop` instruction, but
only if there were no other jump instructions inside the block.  This
means that the following code would still contain a leading `nop` even
though the label following the `nop` is unused:

```ruby
-> { nil if bar }
```

```
== disasm: #<ISeq:block in <main>@test.rb:1 (1,2)-(1,17)> (catch: FALSE)
0000 nop                                                              (   1)[Bc]
0001 putself                                [Li]
0002 opt_send_without_block                 <calldata!mid:bar, argc:0, FCALL|VCALL|ARGS_SIMPLE>
0004 branchunless                           8
0006 putnil
0007 leave                                  [Br]
0008 putnil
0009 leave                                  [Br]
```

This commit checks to see if the label inserted after the `nop` is
actually a jump target.  If it's not a jump target, then we should be
safe to eliminate the leading `nop`:

```
> build-master/miniruby --dump=insns test.rb
== disasm: #<ISeq:<main>@test.rb:1 (1,0)-(1,17)>
0000 putspecialobject                       1                         (   1)[Li]
0002 send                                   <calldata!mid:lambda, argc:0, FCALL>, block in <main>
0005 leave

== disasm: #<ISeq:block in <main>@test.rb:1 (1,2)-(1,17)>
0000 putself                                                          (   1)[LiBc]
0001 opt_send_without_block                 <calldata!mid:bar, argc:0, FCALL|VCALL|ARGS_SIMPLE>
0003 branchunless                           7
0005 putnil
0006 leave                                  [Br]
0007 putnil
0008 leave                                  [Br]
```

We have a test for b_call tracepoints that use `redo` here:

  aebf96f371/test/ruby/test_settracefunc.rb (L1728-L1736)
This commit is contained in:
Aaron Patterson 2025-03-20 09:48:34 -07:00 committed by Aaron Patterson
parent f07af59a2f
commit 62cc3464d9
Notes: git 2025-03-20 18:49:31 +00:00
2 changed files with 30 additions and 3 deletions

View File

@ -4367,9 +4367,18 @@ iseq_optimize(rb_iseq_t *iseq, LINK_ANCHOR *const anchor)
list = FIRST_ELEMENT(anchor);
int do_block_optimization = 0;
LABEL * block_loop_label = NULL;
if (ISEQ_BODY(iseq)->type == ISEQ_TYPE_BLOCK && !ISEQ_COMPILE_DATA(iseq)->catch_except_p) {
// If we're optimizing a block
if (ISEQ_BODY(iseq)->type == ISEQ_TYPE_BLOCK) {
do_block_optimization = 1;
// If the block starts with a nop and a label,
// record the label so we can detect if it's a jump target
LINK_ELEMENT * le = FIRST_ELEMENT(anchor)->next;
if (IS_INSN(le) && IS_INSN_ID((INSN *)le, nop) && IS_LABEL(le->next)) {
block_loop_label = (LABEL *)le->next;
}
}
while (list) {
@ -4386,9 +4395,27 @@ iseq_optimize(rb_iseq_t *iseq, LINK_ANCHOR *const anchor)
if (do_block_optimization) {
INSN * item = (INSN *)list;
if (IS_INSN_ID(item, jump)) {
// Give up if there is a throw
if (IS_INSN_ID(item, throw)) {
do_block_optimization = 0;
}
else {
// If the instruction has a jump target, check if the
// jump target is the block loop label
const char *types = insn_op_types(item->insn_id);
for (int j = 0; types[j]; j++) {
if (types[j] == TS_OFFSET) {
// If the jump target is equal to the block loop
// label, then we can't do the optimization because
// the leading `nop` instruction fires the block
// entry tracepoint
LABEL * target = (LABEL *)OPERAND_AT(item, j);
if (target == block_loop_label) {
do_block_optimization = 0;
}
}
}
}
}
}
if (IS_LABEL(list)) {

View File

@ -1341,7 +1341,7 @@ class TestYJIT < Test::Unit::TestCase
end
def test_tracing_str_uplus
assert_compiles(<<~RUBY, frozen_string_literal: true, result: :ok, exits: { putspecialobject: 1, definemethod: 1 })
assert_compiles(<<~RUBY, frozen_string_literal: true, result: :ok, exits: { putspecialobject: 1 })
def str_uplus
_ = 1
_ = 2