8000 YJIT: Add ability to exit to interpreter from stubs · ruby/ruby@b5b6ab4 · GitHub
[go: up one dir, main page]

Skip to content

Commit b5b6ab4

Browse files
authored
YJIT: Add ability to exit to interpreter from stubs
Previously, YJIT assumed that it's always possible to generate a new basic block when servicing a stub in branch_stub_hit(). When YJIT is out of executable memory, for example, this assumption doesn't hold up. Add handling to branch_stub_hit() for servicing stubs without consuming more executable memory by adding a code path that exits to the interpreter at the location the branch stub represents. The new code path reconstructs interpreter state in branch_stub_hit() and then exits with a new snippet called `code_for_exit_from_stub` that returns `Qundef` from the YJIT native stack frame. As this change adds another place where we regenerate code from `branch_t`, extract the logic for it into a new function and call it regenerate_branch(). While we are at it, make the branch shrinking code path in branch_stub_hit() more explicit. This new functionality is hard to test without full support for out of memory conditions. To verify this change, I ran `RUBY_YJIT_ENABLE=1 make check -j12` with the following patch to stress test the new code path: ```diff diff --git a/yjit_core.c b/yjit_core.c index 4ab63d9806..5788b8c5ed 100644 --- a/yjit_core.c +++ b/yjit_core.c @@ -878,8 +878,12 @@ branch_stub_hit(branch_t *branch, const uint32_t target_idx, rb_execution_contex cb_set_write_ptr(cb, branch->end_addr); } +if (rand() < RAND_MAX/2) { // Compile the new block version p_block = gen_block_version(target, target_ctx, ec); +}else{ + p_block = NULL; +} if (!p_block && branch_modified) { // We couldn't generate a new block for the branch, but we modified the branch. ``` We can enable the new test along with other OOM tests once full support lands. Other small changes: * yjit_utils.c (print_str): Update to work with new native frame shape. Follow up for 8fa0ee4. * yjit_iface.c (rb_yjit_init): Run yjit_init_core() after yjit_init_codegen() so `cb` and `ocb` are available.
1 parent 5943944 commit b5b6ab4

File tree

8 files changed

+154
-55
lines changed

8 files changed

+154
-55
lines changed

bootstraptest/test_yjit.rb

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2434,6 +2434,24 @@ def use(arg)
24342434
A.new.use 1
24352435
}
24362436

2437+
assert_equal 'ok', %q{
2438+
# test hitting a branch stub when out of memory
2439+
def nimai(jita)
2440+
if jita
2441+
:ng
2442+
else
2443+
:ok
2444+
end
2445+
end
2446+
2447+
nimai(true)
2448+
nimai(true)
2449+
2450+
RubyVM::YJIT.simulate_oom! if defined?(RubyVM::YJIT)
2451+
2452+
nimai(false)
2453+
} if false # disabled for now since OOM crashes in the test harness
2454+
24372455
# block invalidation while out of memory
24382456
assert_equal 'new', %q{
24392457
def foo

yjit.c

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,8 @@ YJIT_DECLARE_COUNTERS(
123123
compiled_iseq_count,
124124
compiled_block_count,
125125

126+
exit_from_branch_stub,
127+
126128
invalidation_count,
127129
invalidate_method_lookup,
128130
invalidate_bop_redefined,

yjit_codegen.c

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -382,6 +382,26 @@ yjit_gen_leave_exit(codeblock_t *cb)
382382
return code_ptr;
383383
}
384384

385+
// Fill code_for_exit_from_stub. This is used by branch_stub_hit() to exit
386+
// to the interpreter when it cannot service a stub by generating new code.
387+
// Before coming here, branch_stub_hit() takes care of fully reconstructing
388+
// interpreter state.
389+
static void
390+
gen_code_for_exit_from_stub(void)
391+
{
392+
codeblock_t *cb = ocb;
393+
code_for_exit_from_stub = cb_get_ptr(cb, cb->write_pos);
394+
395+
GEN_COUNTER_INC(cb, exit_from_branch_stub);
396+
397+
pop(cb, REG_SP);
398+
pop(cb, REG_EC);
399+
pop(cb, REG_CFP);
400+
401+
mov(cb, RAX, imm_opnd(Qundef));
402+
ret(cb);
403+
}
404+
385405
// :side-exit:
386406
// Get an exit for the current instruction in the outlined block. The code
387407
// for each instruction often begins with several guards before proceeding

yjit_codegen.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ static uint8_t *yjit_entry_prologue(codeblock_t *cb, const rb_iseq_t *iseq);
1616

1717
static void yjit_gen_block(block_t *block, rb_execution_context_t *ec);
1818

19+
static void gen_code_for_exit_from_stub(void);
20+
1921
static void yjit_init_codegen(void);
2022

2123
#endif // #ifndef YJIT_CODEGEN_H

yjit_core.c

Lines changed: 107 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@
99
#include "yjit_core.h"
1010
#include "yjit_codegen.h"
1111

12+
// For exiting from YJIT frame from branch_stub_hit().
13+
// Filled by gen_code_for_exit_from_stub().
14+
static uint8_t *code_for_exit_from_stub = NULL;
15+
1216
/*
1317
Get an operand for the adjusted stack pointer address
1418
*/
@@ -597,6 +601,52 @@ add_block_version(blockid_t blockid, block_t *block)
597601
#endif
598602
}
599603

604+
static ptrdiff_t
605+
branch_code_size(const branch_t *branch)
606+
{
607+
return branch->end_addr - branch->start_addr;
608+
}
609+
610+
// Generate code for a branch, possibly rewriting and changing the size of it
611+
static void
612+
regenerate_branch(codeblock_t *cb, branch_t *branch)
613+
{
614+
if (branch->start_addr < cb_get_ptr(cb, yjit_codepage_frozen_bytes)) {
615+
// Generating this branch would modify frozen bytes. Do nothing.
616+
return;
617+
}
618+
619+
const uint32_t old_write_pos = cb->write_pos;
620+
const bool branch_terminates_block = branch->end_addr == branch->block->end_addr;
621+
622+
RUBY_ASSERT(branch->dst_addrs[0] != NULL);
623+
624+
cb_set_write_ptr(cb, branch->start_addr);
625+
branch->gen_fn(cb, branch->dst_addrs[0], branch->dst_addrs[1], branch->shape);
626+
branch->end_addr = cb_get_write_ptr(cb);
627+
628+
if (branch_terminates_block) {
629+
// Adjust block size
630+
branch->block->end_addr = branch->end_addr;
631+
}
632+
633+
// cb->write_pos is both a write cursor and a marker for the end of
634+
// everything written out so far. Leave cb->write_pos at the end of the
635+
// block before returning. This function only ever bump or retain the end
636+
// of block marker since that's what the majority of callers want. When the
637+
// branch sits at the very end of the codeblock and it shrinks after
638+
// regeneration, it's up to the caller to drop bytes off the end to
639+
// not leave a gap and implement branch->shape.
640+
if (old_write_pos > cb->write_pos) {
641+
// We rewound cb->write_pos to generate the branch, now restore it.
642+
cb_set_pos(cb, old_write_pos);
643+
}
644+
else {
645+
// The branch sits at the end of cb and consumed some memory.
646+
// Keep cb->write_pos.
647+
}
648+
}
649+
600650
// Create a new outgoing branch entry for a block
601651
static branch_t*
602652
make_branch_entry(block_t *block, const ctx_t *src_ctx, branchgen_fn gen_fn)
@@ -777,13 +827,15 @@ gen_entry_point(const rb_iseq_t *iseq, uint32_t insn_idx, rb_execution_context_t
777827
static uint8_t *
778828
branch_stub_hit(branch_t *branch, const uint32_t target_idx, rb_execution_context_t *ec)
779829
{
780-
uint8_t *dst_addr;
830+
uint8_t *dst_addr = NULL;
781831

782832
// Stop other ractors since we are going to patch machine code.
783833
// This is how the GC does it.
784834
RB_VM_LOCK_ENTER();
785835
rb_vm_barrier();
786836

837+
const ptrdiff_t branch_size_on_entry = branch_code_size(branch);
838+
787839
RUBY_ASSERT(branch != NULL);
788840
RUBY_ASSERT(target_idx < 2);
789841
blockid_t target = branch->targets[target_idx];
@@ -794,18 +846,13 @@ branch_stub_hit(branch_t *branch, const uint32_t target_idx, rb_execution_contex
794846
if (branch->blocks[target_idx]) {
795847
dst_addr = branch->dst_addrs[target_idx];
796848
}
797-
else
798-
{
799-
//fprintf(stderr, "\nstub hit, branch: %p, target idx: %d\n", branch, target_idx);
800-
//fprintf(stderr, "blockid.iseq=%p, blockid.idx=%d\n", target.iseq, target.idx);
801-
//fprintf(stderr, "chain_depth=%d\n", target_ctx->chain_depth);
802-
849+
else {
803850
// :stub-sp-flush:
804851
// Generated code do stack operations without modifying cfp->sp, while the
805852
// cfp->sp tells the GC what values on the stack to root. Generated code
806853
// generally takes care of updating cfp->sp when it calls runtime routines that
807-
// could trigger GC, but for the case of branch stubs, it's inconvenient. So
808-
// we do it here.
854+
// could trigger GC, but it's inconvenient to do it before calling this function.
855+
// So we do it here instead.
809856
VALUE *const original_interp_sp = ec->cfp->sp;
810857
ec->cfp->sp += target_ctx->sp_offset;
811858

@@ -818,52 +865,74 @@ branch_stub_hit(branch_t *branch, const uint32_t target_idx, rb_execution_contex
818865

819866
// If this block hasn't yet been compiled
820867
if (!p_block) {
868+
const uint8_t branch_old_shape = branch->shape;
869+
bool branch_modified = false;
870+
821871
// If the new block can be generated right after the branch (at cb->write_pos)
822-
if (cb_get_write_ptr(cb) == branch->end_addr && branch->start_addr >= cb_get_ptr(cb, yjit_codepage_frozen_bytes)) {
872+
if (cb_get_write_ptr(cb) == branch->end_addr) {
823873
// This branch should be terminating its block
824874
RUBY_ASSERT(branch->end_addr == branch->block->end_addr);
825875

826876
// Change the branch shape to indicate the target block will be placed next
827877
branch->shape = (uint8_t)target_idx;
828878

829879
// Rewrite the branch with the new, potentially more compact shape
830-
cb_set_write_ptr(cb, branch->start_addr);
831-
branch->gen_fn(cb, branch->dst_addrs[0], branch->dst_addrs[1], branch->shape);
832-
RUBY_ASSERT(cb_get_write_ptr(cb) <= branch->end_addr && "can't enlarge branches");
833-
branch->end_addr = cb_get_write_ptr(cb);
834-
branch->block->end_addr = cb_get_write_ptr(cb);
880+
regenerate_branch(cb, branch);
881+
branch_modified = true;
882+
883+
// Ensure that the branch terminates the codeblock just like
884+
// before entering this if block. This drops bytes off the end
885+
// in case we shrank the branch when regenerating.
886+
cb_set_write_ptr(cb, branch->end_addr);
835887
}
836888

837889
// Compile the new block version
838890
p_block = gen_block_version(target, target_ctx, ec);
839-
RUBY_ASSERT(p_block);
840-
RUBY_ASSERT(!(branch->shape == (uint8_t)target_idx && p_block->start_addr != branch->end_addr));
891+
892+
if (!p_block && branch_modified) {
893+
// We couldn't generate a new block for the branch, but we modified the branch.
894+
// Restore the branch by regenerating it.
895+
branch->shape = branch_old_shape;
896+
regenerate_branch(cb, branch);
897+
}
841898
}
842899

843-
// Add this branch to the list of incoming branches for the target
844-
rb_darray_append(&p_block->incoming, branch);
900+
if (p_block) {
901+
// Branch shape should reflect layout
902+
RUBY_ASSERT(!(branch->shape == (uint8_t)target_idx && p_block->start_addr != branch->end_addr));
845903

846-
// Update the branch target address
847-
dst_addr = p_block->start_addr;
848-
branch->dst_addrs[target_idx] = dst_addr;
904+
// Add this branch to the list of incoming branches for the target
905+
rb_darray_append(&p_block->incoming, branch);
849906

850-
// Rewrite the branch with the new jump target address
851-
if (branch->start_addr >= cb_get_ptr(cb, yjit_codepage_frozen_bytes)) {
852-
RUBY_ASSERT(branch->dst_addrs[0] != NULL);
853-
uint32_t cur_pos = cb->write_pos;
854-
cb_set_write_ptr(cb, branch->start_addr);
855-
branch->gen_fn(cb, branch->dst_addrs[0], branch->dst_addrs[1], branch->shape);
856-
RUBY_ASSERT(cb_get_write_ptr(cb) == branch->end_addr && "branch can't change size");
857-
cb_set_pos(cb, cur_pos);
858-
}
907+
// Update the branch target address
908+
dst_addr = p_block->start_addr;
909+
branch->dst_addrs[target_idx] = dst_addr;
859910

860-
// Mark this branch target as patched (no longer a stub)
861-
branch->blocks[target_idx] = p_block;
911+
// Mark this branch target as patched (no longer a stub)
912+
branch->blocks[target_idx] = p_block;
862913

863-
// Restore interpreter sp, since the code hitting the stub expects the original.
864-
ec->cfp->sp = original_interp_sp;
914+
// Rewrite the branch with the new jump target address
915+
regenerate_branch(cb, branch);
916+
917+
// Restore interpreter sp, since the code hitting the stub expects the original.
918+
ec->cfp->sp = original_interp_sp;
919+
}
920+
else {
921+
// Failed to service the stub by generating a new block so now we
922+
// need to exit to the interpreter at the stubbed location. We are
923+
// intentionally *not* restoring original_interp_sp. At the time of
924+
// writing, reconstructing interpreter state only involves setting
925+
// cfp->sp and cfp->pc. We set both before trying to generate the
926+
// block. All there is left to do to exit is to pop the native
927+
// frame. We do that in code_for_exit_from_stub.
928+
dst_addr = code_for_exit_from_stub;
929+
}
865930
}
866931

932+
const ptrdiff_t new_branch_size = branch_code_size(branch);
933+
RUBY_ASSERT_ALWAYS(new_branch_size >= 0);
934+
RUBY_ASSERT_ALWAYS(new_branch_size <= branch_size_on_entry && "branch stubs should not enlarge branches");
935+
867936
RB_VM_LOCK_LEAVE();
868937

869938
// Return a pointer to the compiled block version
@@ -942,8 +1011,7 @@ gen_branch(
9421011

9431012
// Call the branch generation function
9441013
branch->start_addr = cb_get_write_ptr(cb);
945-
gen_fn(cb, branch->dst_addrs[0], branch->dst_addrs[1], SHAPE_DEFAULT);
946-
branch->end_addr = cb_get_write_ptr(cb);
1014+
regenerate_branch(cb, branch);
9471015
}
9481016

9491017
static void
@@ -1191,13 +1259,7 @@ invalidate_block_version(block_t *block)
11911259
}
11921260

11931261
// Rewrite the branch with the new jump target address
1194-
RUBY_ASSERT(branch->dst_addrs[0] != NULL);
1195-
uint32_t cur_pos = cb->write_pos;
1196-
cb_set_write_ptr(cb, branch->start_addr);
1197-
branch->gen_fn(cb, branch->dst_addrs[0], branch->dst_addrs[1], branch->shape);
1198-
branch->end_addr = cb_get_write_ptr(cb);
1199-
branch->block->end_addr = cb_get_write_ptr(cb);
1200-
cb_set_pos(cb, cur_pos);
1262+
regenerate_branch(cb, branch);
12011263

12021264
if (target_next && branch->end_addr > block->end_addr) {
12031265
fprintf(stderr, "branch_block_idx=%u block_idx=%u over=%ld block_size=%ld\n",
@@ -1243,5 +1305,5 @@ invalidate_block_version(block_t *block)
12431305
static void
12441306
yjit_init_core(void)
12451307
{
1246-
// Nothing yet
1308+
gen_code_for_exit_from_stub();
12471309
}

yjit_core.h

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -192,8 +192,8 @@ typedef struct yjit_branch_entry
192192
struct yjit_block_version *block;
193193

194194
// Positions where the generated code starts and ends
195-
uint8_t* start_addr;
196-
uint8_t* end_addr;
195+
uint8_t *start_addr;
196+
uint8_t *end_addr;
197197

198198
// Context right after the branch instruction
199199
ctx_t src_ctx;
@@ -204,7 +204,7 @@ typedef struct yjit_branch_entry
204204
struct yjit_block_version *blocks[2];
205205

206206
// Jump target addresses
207-
uint8_t* dst_addrs[2];
207+
uint8_t *dst_addrs[2];
208208

209209
// Branch code generation function
210210
branchgen_fn gen_fn;

yjit_iface.c

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1232,17 +1232,16 @@ rb_yjit_init(struct rb_yjit_options *options)
12321232
}
12331233

12341234
// If type propagation is disabled, max 1 version per block
1235-
if (rb_yjit_opts.no_type_prop)
1236-
{
1235+
if (rb_yjit_opts.no_type_prop) {
12371236
rb_yjit_opts.max_versions = 1;
12381237
}
12391238

12401239
blocks_assuming_stable_global_constant_state = st_init_numtable();
12411240
blocks_assuming_single_ractor_mode = st_init_numtable();
12421241
blocks_assuming_bops = st_init_numtable();
12431242

1244-
yjit_init_core();
12451243
yjit_init_codegen();
1244+
yjit_init_core();
12461245

12471246
// YJIT Ruby module
12481247
mYjit = rb_define_module_under(rb_cRubyVM, "YJIT");

yjit_utils.c

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -101,13 +101,9 @@ print_str(codeblock_t *cb, const char *str)
101101
cb_write_byte(cb, (uint8_t)str[i]);
102102
cb_write_byte(cb, 0);
103103

104-
push(cb, RSP); // Alignment
105-
106104
// Call the print function
107105
mov(cb, RAX, const_ptr_opnd((void*)&print_str_cfun));
108106
call(cb, RAX);
109107

110-
pop(cb, RSP); // Alignment
111-
112108
pop_regs(cb);
113109
}

0 commit comments

Comments
 (0)
0