ZJIT: unify hir::Insn::{Send,SendWithoutBlock}#16143
Conversation
After ruby#15911, we largely apply the same treatment to these two variants everywhere. In `type_specialize`, there are two ~100 LOC match arms that shared 90% of the code. Unifying these allowed the code to be shared more easily. --- **Note:** In the interest of preserving existing semantics and keeping the diff small for this initial PR, `YARVINSN_send` can currently produce: ```rs Insn::Send { blockiseq: Some(null_ptr), .. } ``` This basically happens for cases like `foo(&:bar)` where YARV opted not to specialize into `YARVINSN_opt_send_without_block`. Inside `type_specialize`, we check for these conditions (indirectly, not by matching `Some(null_ptr)`) and bail, so they ended up remaining `Insn::Send` throughout, and goes through `rb_vm_send` which allows for `blockiseq: null_ptr`. There isn't a _great_ reason to do this, as far as I can tell. At minimum, it seems like a pretty backhanded way to thread this, but also nothing seemed to care, as we can re-derive the information via ci flags. It was a small change to make this case `blockiseq: None` instead. For the most part, everything works (some cosmetic snapshot diffs), except that we need to somehow avoid routing these cases through `rb_vm_opt_send_without_block`. Options: 1. Accept that unspecialized `Send` go through `rb_vm_send` 2. Check ci flags in `gen_send` to decide between `rb_vm_send` or `rb_vm_opt_send_without_block` 3. Bring back `Insn::SendWithoutBlock`, but much narrower – as a possible specialization we emit in `type_specialize`, rather than as an input to the funnel like it was previously Either way, that can be done in a follow-up PR. --- Follow-up tasks: 1. Unify `reduce_send_to_ccall`/`reduce_send_without_block_to_ccall` 2. Address the `Some(null_ptr)` situation above 3. Rename/merge the stats/counters/fallback reasons, if desired Partially addresses Shopify#941 (TODO: `optimize_c_calls`)
hir::Insn::{Send,SendWithoutBlock}hir::Insn::{Send,SendWithoutBlock}
|
Fixed the syntax error on older rustc. Questions for follow-ups
|
| let args = state.stack_pop_n(argc as usize + usize::from(block_arg))?; | ||
| let recv = state.stack_pop()?; | ||
| let send = fun.push_insn(block, Insn::Send { recv, cd, blockiseq, args, state: exit_id, reason: Uncategorized(opcode) }); | ||
| let send = fun.push_insn(block, Insn::Send { recv, cd, blockiseq: Some(blockiseq), args, state: exit_id, reason: Uncategorized(opcode) }); |
There was a problem hiding this comment.
This is where the Some(null_ptr) is constructed – see is_null() check below
There was a problem hiding this comment.
For the review, the mental model of the current state is that blockiseq is really an enum of three possible states: Some(blockiseq), Some(null) and None (previously SendWithoutBlock).
Mechanically:
- previous
SendWithoutBlockarms becameSend { blockiseq: None }arms - previous
Sendarms becameSend { blockiseq: Some(_) }arms - within the
Sendarms, wherever we previously cared aboutis_null(), we still need to keep checking.
As long as we do all three, the before/after should be exactly equivalent.
Don't think that's a good state to leave things in the long term, but it kept the diff 1:1 for the initial review, and I'll take directions on how we want to further refactor/evolve this afterwards.
Requires 1.88, see rust-lang/rust#53667
e059520 to
6e8e6c5
Compare
This comment has been minimized.
This comment has been minimized.
XrXr
left a comment
There was a problem hiding this comment.
Re: Some(null_ptr) situation, the Option is acting as signal for the provenance of the HIR instruction. It is backhanded way to represent this.
Options: Accept that all unspecialized Send go through rb_vm_send
Check ci flags in gen_send to decide between rb_vm_send or rb_vm_opt_send_without_block
Bring back Insn::SendWithoutBlock, but much narrower – as a possible specialization we emit in type_specialize, rather than as an input to the funnel like it was previously
The there's strict correspondence between the fallback function used and the specific opcode we need to maintain. That rules out the first two options. We also want to stick to just one send instruction.
We could look at the frame state and find the interpreter opcode through the PC, but it feels better to parse this onto the HIR instruction as a field. So, raw pointer for blockiseq, and replace the Option with an explicitly named boolean field.
This basically happens for cases like foo(&:bar) where YARV opted not to specialize into YARVINSN_opt_send_without_block.
This is VM_CALL_ARGS_BLOCKARG; it's still sending with a block, just not through blockiseq. So blockiseq is nil in these cases.
|
Updated now, should be ready to merge. Context: the original runtime crash I eventually found had to do with passing a bogus block handler to Enumerable, but later I realized the issue can be simplified – it doesn't actually matter that there is or isn't a block, the issue is that with To get it to crash, you'd have to do something with that block handler (yielding), but for the purpose of the hir snapshot test, just having/not having a block in the resulting hir is sufficient to catch the regression. I neglected to update the comment after the long session and ended up confusing myself on the second read. Updated the comment. All good now. |
|
Note: these are discussions for post-merge follow-up tasks
@XrXr I think given my last comment, I'm not sure the provenance (I took that to mean "which YARV instructions did it originate from") is what we actually want to capture or a reliable/useful signal. Here is my current understanding based on reading code and interpreting your comment. Please correct me if I'm getting some details wrong: What we really want to capture here: no block passed, passing an iseq block, passing a non-iseq block. Can we use an With
This is fine. But it seems like we are also required to handle This is where we were supposed to do the triage in the Somewhat coincidentally, it mostly didn't turn out to be a big deal – for the most part, Except in the case of the cfunc path, where it happens to propagate the untriaged So there are really two things:
|
|
Using an enum sounds good to me. In case of
Yes. We need to do this anyways because presence of
This is just for in case of falling back and we actually need to execute the |
|
Sounds good to me, happy to do that after merge, I think this current PR preserved the current imperfect state exactly as it was before |
|
@XrXr sorry, one more Q: do we want to leave the fallback reasons/counters as |
|
Let's merge them. I can't think of a situation where the distinction was actually useful and IIRC Kokubun mentioned wanting to merge them. |
After rubyGH-15911, we largely apply the same treatment to these two variants everywhere. In `type_specialize`, there are two ~100 LOC match arms that shared 90% of the code. Unifying these allowed the code to be shared more easily. --- **Note:** In the interest of preserving existing semantics and keeping the diff small for this initial PR, `YARVINSN_send` can currently produce: ```rs Insn::Send { blockiseq: Some(null_ptr), .. } ``` Either way, that can be done in a follow-up PR. --- Follow-up tasks: 1. Unify `reduce_send_to_ccall`/`reduce_send_without_block_to_ccall` 2. Address the `Some(null_ptr)` situation above (discussed on PR) 3. Rename/merge the stats/counters/fallback reasons, if desired Partially addresses #941 (TODO: `optimize_c_calls`) [alan: rewrote commit message] Reviewed-by: Alan Wu <XrXr@users.noreply.github.com>
As agreed in ruby#16143, unify the duplicated SendFallbackReason variants and stats counters. SendWithoutBlock{Polymorphic,Megamorphic,...} are merged into Send{Polymorphic,Megamorphic,...}, removing the distinction that was no longer useful after Send and SendWithoutBlock instructions were unified.
…Send Introduce a SendBlock enum with variants None, Literal(IseqPtr), and BlockArg(InsnId) to properly represent the three block argument states on Insn::Send. The None variant carries a bool to track YARV opcode provenance (opt_send_without_block vs send) for fallback dispatch. The YARV_send handler now triages ci flags to set the correct variant, fixing a latent bug where YARVINSN_send with null blockiseq (e.g. from specialized_instruction: false) was conflated with having a block. Follow-up ruby#2 from ruby#16143
As agreed in ruby#16143, unify the Send/SendWithoutBlockFallbackReason variants and counters. Purely mechanical change.
Follow-up from ruby#16143 Introduce a `SendBlock` to track all possible states of the block arg in `Send` family instructions. Also track YARV opcode provenance (opt_send_without_block vs send) for fallback dispatch. The `YARVINSN_send` handler now properly triages ci flags to set the correct variant. This fixed at least one case when `specialized_instruction: false` with a null blockiseq was conflated with having a block, and possibly other cases where the wrong fallback path was used previously.
As agreed in ruby#16143, unify the Send/SendWithoutBlockFallbackReason variants and counters. Purely mechanical change.
Follow-up from ruby#16143 Introduce a `SendBlock` to track all possible states of the block arg in `Send` family instructions. Also track YARV opcode provenance (opt_send_without_block vs send) for fallback dispatch. The `YARVINSN_send` handler now properly triages ci flags to set the correct variant. This fixed at least one case when `specialized_instruction: false` with a null blockiseq was conflated with having a block, and possibly other cases where the wrong fallback path was used previously.
Follow-up from ruby#16143 Introduce a `SendBlock` to track all possible states of the block arg in `Send` family instructions. Also track YARV opcode provenance (opt_send_without_block vs send) for fallback dispatch. The `YARVINSN_send` handler now properly triages ci flags to set the correct variant. This fixed at least one case when `specialized_instruction: false` with a null blockiseq was conflated with having a block, and possibly other cases where the wrong fallback path was used previously.
After #15911, we largely apply the same treatment to these two variants everywhere. In
type_specialize, there are two ~100 LOC match arms that shared 90% of the code. Unifying these allowed the code to be shared more easily.Note:
In the interest of preserving existing semantics and keeping the diff small for this initial PR,
YARVINSN_sendcan currently produce:This basically happens for cases like
foo(&:bar)where YARV opted not to specialize intoYARVINSN_opt_send_without_block. Insidetype_specialize, we check for these conditions (indirectly, not by matchingSome(null_ptr)) and bail, so they ended up remainingInsn::Sendthroughout, and goes throughrb_vm_sendwhich allows forblockiseq: null_ptr.There isn't a great reason to do this, as far as I can tell. At minimum, it seems like a pretty backhanded way to thread this, but also nothing seemed to care, as we can re-derive the information via ci flags.
It was a small change to make this case
blockiseq: Noneinstead. For the most part, everything works (some cosmetic snapshot diffs), except that we need to somehow avoid routing these cases throughrb_vm_opt_send_without_block.Options:
Accept that all unspecialized
Sendgo throughrb_vm_sendCheck ci flags in
gen_sendto decide betweenrb_vm_sendorrb_vm_opt_send_without_blockBring back
Insn::SendWithoutBlock, but much narrower – as a possible specialization we emit intype_specialize, rather than as an input to the funnel like it was previouslyEither way, that can be done in a follow-up PR.
Follow-up tasks:
Unify
reduce_send_to_ccall/reduce_send_without_block_to_ccallAddress the
Some(null_ptr)situation aboveRename/merge the stats/counters/fallback reasons, if desired
Partially addresses Shopify#941 (TODO:
optimize_c_calls)