Skip to content

Commit cd8d20c

Browse files
k0kubunmaximecb
andauthored
YJIT: Compile exception handlers (#8171)
Co-authored-by: Maxime Chevalier-Boisvert <maximechevalierb@gmail.com>
1 parent 74b9c7d commit cd8d20c

File tree

16 files changed

+274
-85
lines changed

16 files changed

+274
-85
lines changed

bootstraptest/test_yjit.rb

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4004,3 +4004,25 @@ def calling_func
40044004
_x, _y = func.call
40054005
end.call
40064006
}
4007+
4008+
# Catch TAG_BREAK in a non-FINISH frame with JIT code
4009+
assert_equal '1', %q{
4010+
def entry
4011+
catch_break
4012+
end
4013+
4014+
def catch_break
4015+
while_true do
4016+
break
4017+
end
4018+
1
4019+
end
4020+
4021+
def while_true
4022+
while true
4023+
yield
4024+
end
4025+
end
4026+
4027+
entry
4028+
}

lib/ruby_vm/rjit/compiler.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ def compile(iseq, cfp)
6464
asm = Assembler.new
6565
compile_prologue(asm, iseq, pc)
6666
compile_block(asm, jit:, pc:)
67-
iseq.body.jit_func = @cb.write(asm)
67+
iseq.body.jit_entry = @cb.write(asm)
6868
rescue Exception => e
6969
$stderr.puts e.full_message
7070
exit 1
@@ -176,8 +176,8 @@ def invalidate_blocks(iseq, pc)
176176

177177
# If they were the ISEQ's first blocks, re-compile RJIT entry as well
178178
if iseq.body.iseq_encoded.to_i == pc
179-
iseq.body.jit_func = 0
180-
iseq.body.total_calls = 0
179+
iseq.body.jit_entry = 0
180+
iseq.body.jit_entry_calls = 0
181181
end
182182
end
183183

lib/ruby_vm/rjit/invariants.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -143,11 +143,11 @@ def invalidate_all
143143

144144
C.rjit_for_each_iseq do |iseq|
145145
# Avoid entering past code
146-
iseq.body.jit_func = 0
146+
iseq.body.jit_entry = 0
147147
# Avoid reusing past code
148148
iseq.body.rjit_blocks.clear if iseq.body.rjit_blocks
149149
# Compile this again if not converted to trace_* insns
150-
iseq.body.total_calls = 0
150+
iseq.body.jit_entry_calls = 0
151151
end
152152
end
153153
end

rjit_c.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1177,8 +1177,8 @@ def C.rb_iseq_constant_body
11771177
), Primitive.cexpr!("OFFSETOF((*((struct rb_iseq_constant_body *)NULL)), mark_bits)")],
11781178
outer_variables: [CType::Pointer.new { self.rb_id_table }, Primitive.cexpr!("OFFSETOF((*((struct rb_iseq_constant_body *)NULL)), outer_variables)")],
11791179
mandatory_only_iseq: [CType::Pointer.new { self.rb_iseq_t }, Primitive.cexpr!("OFFSETOF((*((struct rb_iseq_constant_body *)NULL)), mandatory_only_iseq)")],
1180-
jit_func: [self.rb_jit_func_t, Primitive.cexpr!("OFFSETOF((*((struct rb_iseq_constant_body *)NULL)), jit_func)")],
1181-
total_calls: [CType::Immediate.parse("unsigned long"), Primitive.cexpr!("OFFSETOF((*((struct rb_iseq_constant_body *)NULL)), total_calls)")],
1180+
jit_entry: [self.rb_jit_func_t, Primitive.cexpr!("OFFSETOF((*((struct rb_iseq_constant_body *)NULL)), jit_entry)")],
1181+
jit_entry_calls: [CType::Immediate.parse("unsigned long"), Primitive.cexpr!("OFFSETOF((*((struct rb_iseq_constant_body *)NULL)), jit_entry_calls)")],
11821182
rjit_blocks: [self.VALUE, Primitive.cexpr!("OFFSETOF((*((struct rb_iseq_constant_body *)NULL)), rjit_blocks)"), true],
11831183
)
11841184
end

tool/rjit/bindgen.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -638,7 +638,7 @@ def push_target(target)
638638
skip_fields: {
639639
'rb_execution_context_struct.machine': %w[regs], # differs between macOS and Linux
640640
rb_execution_context_struct: %w[method_missing_reason], # non-leading bit fields not supported
641-
rb_iseq_constant_body: %w[yjit_payload], # conditionally defined
641+
rb_iseq_constant_body: %w[jit_exception jit_exception_calls yjit_payload], # conditionally defined
642642
rb_thread_struct: %w[status has_dedicated_nt to_kill abort_on_exception report_on_exception pending_interrupt_queue_checked],
643643
:'' => %w[is_from_method is_lambda is_isolated], # rb_proc_t
644644
},

vm.c

Lines changed: 72 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -370,7 +370,14 @@ extern VALUE rb_vm_invoke_bmethod(rb_execution_context_t *ec, rb_proc_t *proc, V
370370
static VALUE vm_invoke_proc(rb_execution_context_t *ec, rb_proc_t *proc, VALUE self, int argc, const VALUE *argv, int kw_splat, VALUE block_handler);
371371

372372
#if USE_RJIT || USE_YJIT
373-
// Try to compile the current ISeq in ec. Return 0 if not compiled.
373+
// Generate JIT code that supports the following kinds of ISEQ entries:
374+
// * The first ISEQ on vm_exec (e.g. <main>, or Ruby methods/blocks
375+
// called by a C method). The current frame has VM_FRAME_FLAG_FINISH.
376+
// The current vm_exec stops if JIT code returns a non-Qundef value.
377+
// * ISEQs called by the interpreter on vm_sendish (e.g. Ruby methods or
378+
// blocks called by a Ruby frame that isn't compiled or side-exited).
379+
// The current frame doesn't have VM_FRAME_FLAG_FINISH. The current
380+
// vm_exec does NOT stop whether JIT code returns Qundef or not.
374381
static inline rb_jit_func_t
375382
jit_compile(rb_execution_context_t *ec)
376383
{
@@ -379,35 +386,29 @@ jit_compile(rb_execution_context_t *ec)
379386
struct rb_iseq_constant_body *body = ISEQ_BODY(iseq);
380387
bool yjit_enabled = rb_yjit_compile_new_iseqs();
381388
if (yjit_enabled || rb_rjit_call_p) {
382-
body->total_calls++;
389+
body->jit_entry_calls++;
383390
}
384391
else {
385-
return 0;
386-
}
387-
388-
// Don't try to compile the function if it's already compiled
389-
if (body->jit_func) {
390-
return body->jit_func;
392+
return NULL;
391393
}
392394

393-
// Trigger JIT compilation as needed
394-
if (yjit_enabled) {
395-
if (rb_yjit_threshold_hit(iseq)) {
396-
rb_yjit_compile_iseq(iseq, ec);
395+
// Trigger JIT compilation if not compiled
396+
if (body->jit_entry == NULL) {
397+
if (yjit_enabled) {
398+
if (rb_yjit_threshold_hit(iseq, body->jit_entry_calls)) {
399+
rb_yjit_compile_iseq(iseq, ec, false);
400+
}
397401
}
398-
}
399-
else { // rb_rjit_call_p
400-
if (body->total_calls == rb_rjit_call_threshold()) {
401-
rb_rjit_compile(iseq);
402+
else { // rb_rjit_call_p
403+
if (body->jit_entry_calls == rb_rjit_call_threshold()) {
404+
rb_rjit_compile(iseq);
405+
}
402406
}
403407
}
404-
405-
return body->jit_func;
408+
return body->jit_entry;
406409
}
407410

408-
// Try to execute the current iseq in ec. Use JIT code if it is ready.
409-
// If it is not, add ISEQ to the compilation queue and return Qundef for RJIT.
410-
// YJIT compiles on the thread running the iseq.
411+
// Execute JIT code compiled by jit_compile()
411412
static inline VALUE
412413
jit_exec(rb_execution_context_t *ec)
413414
{
@@ -425,6 +426,51 @@ jit_exec(rb_execution_context_t *ec)
425426
# define jit_exec(ec) Qundef
426427
#endif
427428

429+
#if USE_YJIT
430+
// Generate JIT code that supports the following kind of ISEQ entry:
431+
// * The first ISEQ pushed by vm_exec_handle_exception. The frame would
432+
// point to a location specified by a catch table, and it doesn't have
433+
// VM_FRAME_FLAG_FINISH. The current vm_exec stops if JIT code returns
434+
// a non-Qundef value. So you should not return a non-Qundef value
435+
// until ec->cfp is changed to a frame with VM_FRAME_FLAG_FINISH.
436+
static inline rb_jit_func_t
437+
jit_compile_exception(rb_execution_context_t *ec)
438+
{
439+
// Increment the ISEQ's call counter
440+
const rb_iseq_t *iseq = ec->cfp->iseq;
441+
struct rb_iseq_constant_body *body = ISEQ_BODY(iseq);
442+
if (rb_yjit_compile_new_iseqs()) {
443+
body->jit_exception_calls++;
444+
}
445+
else {
446+
return NULL;
447+
}
448+
449+
// Trigger JIT compilation if not compiled
450+
if (body->jit_exception == NULL && rb_yjit_threshold_hit(iseq, body->jit_exception_calls)) {
451+
rb_yjit_compile_iseq(iseq, ec, true);
452+
}
453+
return body->jit_exception;
454+
}
455+
456+
// Execute JIT code compiled by jit_compile_exception()
457+
static inline VALUE
458+
jit_exec_exception(rb_execution_context_t *ec)
459+
{
460+
rb_jit_func_t func = jit_compile_exception(ec);
461+
if (func) {
462+
// Call the JIT code
463+
return func(ec, ec->cfp);
464+
}
465+
else {
466+
return Qundef;
467+
}
468+
}
469+
#else
470+
# define jit_compile_exception(ec) ((rb_jit_func_t)0)
471+
# define jit_exec_exception(ec) Qundef
472+
#endif
473+
428474
#include "vm_insnhelper.c"
429475

430476
#include "vm_exec.c"
@@ -2381,8 +2427,11 @@ vm_exec_loop(rb_execution_context_t *ec, enum ruby_tag_type state,
23812427

23822428
rb_ec_raised_reset(ec, RAISED_STACKOVERFLOW | RAISED_NOMEMORY);
23832429
while (UNDEF_P(result = vm_exec_handle_exception(ec, state, result))) {
2384-
/* caught a jump, exec the handler */
2385-
result = vm_exec_core(ec);
2430+
// caught a jump, exec the handler. JIT code in jit_exec_exception()
2431+
// may return Qundef to run remaining frames with vm_exec_core().
2432+
if (UNDEF_P(result = jit_exec_exception(ec))) {
2433+
result = vm_exec_core(ec);
2434+
}
23862435
vm_loop_start:
23872436
VM_ASSERT(ec->tag == tag);
23882437
/* when caught `throw`, `tag.state` is set. */

vm_core.h

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -503,10 +503,17 @@ struct rb_iseq_constant_body {
503503
const rb_iseq_t *mandatory_only_iseq;
504504

505505
#if USE_RJIT || USE_YJIT
506-
// Function pointer for JIT code
507-
rb_jit_func_t jit_func;
508-
// Number of total calls with jit_exec()
509-
long unsigned total_calls;
506+
// Function pointer for JIT code on jit_exec()
507+
rb_jit_func_t jit_entry;
508+
// Number of calls on jit_exec()
509+
long unsigned jit_entry_calls;
510+
#endif
511+
512+
#if USE_YJIT
513+
// Function pointer for JIT code on jit_exec_exception()
514+
rb_jit_func_t jit_exception;
515+
// Number of calls on jit_exec_exception()
516+
long unsigned jit_exception_calls;
510517
#endif
511518

512519
#if USE_RJIT

vm_insnhelper.c

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2485,6 +2485,12 @@ vm_base_ptr(const rb_control_frame_t *cfp)
24852485
}
24862486
}
24872487

2488+
VALUE *
2489+
rb_vm_base_ptr(const rb_control_frame_t *cfp)
2490+
{
2491+
return vm_base_ptr(cfp);
2492+
}
2493+
24882494
/* method call processes with call_info */
24892495

24902496
#include "vm_args.c"

yjit.c

Lines changed: 43 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -422,10 +422,12 @@ void
422422
rb_iseq_reset_jit_func(const rb_iseq_t *iseq)
423423
{
424424
RUBY_ASSERT_ALWAYS(IMEMO_TYPE_P(iseq, imemo_iseq));
425-
iseq->body->jit_func = NULL;
425+
iseq->body->jit_entry = NULL;
426+
iseq->body->jit_exception = NULL;
426427
// Enable re-compiling this ISEQ. Event when it's invalidated for TracePoint,
427428
// we'd like to re-compile ISEQs that haven't been converted to trace_* insns.
428-
iseq->body->total_calls = 0;
429+
iseq->body->jit_entry_calls = 0;
430+
iseq->body->jit_exception_calls = 0;
429431
}
430432

431433
// Get the PC for a given index in an iseq
@@ -597,12 +599,6 @@ rb_get_def_bmethod_proc(rb_method_definition_t *def)
597599
return def->body.bmethod.proc;
598600
}
599601

600-
unsigned long
601-
rb_get_iseq_body_total_calls(const rb_iseq_t *iseq)
602-
{
603-
return iseq->body->total_calls;
604-
}
605-
606602
const rb_iseq_t *
607603
rb_get_iseq_body_local_iseq(const rb_iseq_t *iseq)
608604
{
@@ -832,6 +828,8 @@ rb_get_cfp_ep_level(struct rb_control_frame_struct *cfp, uint32_t lv)
832828
return ep;
833829
}
834830

831+
extern VALUE *rb_vm_base_ptr(struct rb_control_frame_struct *cfp);
832+
835833
VALUE
836834
rb_yarv_class_of(VALUE obj)
837835
{
@@ -1047,27 +1045,24 @@ rb_yjit_vm_unlock(unsigned int *recursive_lock_level, const char *file, int line
10471045
rb_vm_lock_leave(recursive_lock_level, file, line);
10481046
}
10491047

1050-
bool
1051-
rb_yjit_compile_iseq(const rb_iseq_t *iseq, rb_execution_context_t *ec)
1048+
void
1049+
rb_yjit_compile_iseq(const rb_iseq_t *iseq, rb_execution_context_t *ec, bool jit_exception)
10521050
{
1053-
bool success = true;
10541051
RB_VM_LOCK_ENTER();
10551052
rb_vm_barrier();
10561053

1057-
// Compile a block version starting at the first instruction
1058-
uint8_t *rb_yjit_iseq_gen_entry_point(const rb_iseq_t *iseq, rb_execution_context_t *ec); // defined in Rust
1059-
uint8_t *code_ptr = rb_yjit_iseq_gen_entry_point(iseq, ec);
1054+
// Compile a block version starting at the current instruction
1055+
uint8_t *rb_yjit_iseq_gen_entry_point(const rb_iseq_t *iseq, rb_execution_context_t *ec, bool jit_exception); // defined in Rust
1056+
uint8_t *code_ptr = rb_yjit_iseq_gen_entry_point(iseq, ec, jit_exception);
10601057

1061-
if (code_ptr) {
1062-
iseq->body->jit_func = (rb_jit_func_t)code_ptr;
1058+
if (jit_exception) {
1059+
iseq->body->jit_exception = (rb_jit_func_t)code_ptr;
10631060
}
10641061
else {
1065-
iseq->body->jit_func = 0;
1066-
success = false;
1062+
iseq->body->jit_entry = (rb_jit_func_t)code_ptr;
10671063
}
10681064

10691065
RB_VM_LOCK_LEAVE();
1070-
return success;
10711066
}
10721067

10731068
// GC root for interacting with the GC
@@ -1143,6 +1138,35 @@ rb_yjit_invokeblock_sp_pops(const struct rb_callinfo *ci)
11431138
return 1 - sp_inc_of_invokeblock(ci); // + 1 to ignore return value push
11441139
}
11451140

1141+
// Setup jit_return to avoid returning a non-Qundef value on a non-FINISH frame.
1142+
// See [jit_compile_exception] for details.
1143+
void
1144+
rb_yjit_set_exception_return(rb_control_frame_t *cfp, void *leave_exit, void *leave_exception)
1145+
{
1146+
if (VM_FRAME_FINISHED_P(cfp)) {
1147+
// If it's a FINISH frame, just normally exit with a non-Qundef value.
1148+
cfp->jit_return = leave_exit;
1149+
}
1150+
else if (cfp->jit_return) {
1151+
while (!VM_FRAME_FINISHED_P(cfp)) {
1152+
if (cfp->jit_return == leave_exit) {
1153+
// Unlike jit_exec(), leave_exit is not safe on a non-FINISH frame on
1154+
// jit_exec_exception(). See [jit_exec] and [jit_exec_exception] for
1155+
// details. Exit to the interpreter with Qundef to let it keep executing
1156+
// other Ruby frames.
1157+
cfp->jit_return = leave_exception;
1158+
return;
1159+
}
1160+
cfp = RUBY_VM_PREVIOUS_CONTROL_FRAME(cfp);
1161+
}
1162+
}
1163+
else {
1164+
// If the caller was not JIT code, exit to the interpreter with Qundef
1165+
// to keep executing Ruby frames with the interpreter.
1166+
cfp->jit_return = leave_exception;
1167+
}
1168+
}
1169+
11461170
// Primitives used by yjit.rb
11471171
VALUE rb_yjit_stats_enabled_p(rb_execution_context_t *ec, VALUE self);
11481172
VALUE rb_yjit_trace_exit_locations_enabled_p(rb_execution_context_t *ec, VALUE self);

yjit.h

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,12 @@
2727
// Expose these as declarations since we are building YJIT.
2828
bool rb_yjit_enabled_p(void);
2929
bool rb_yjit_compile_new_iseqs(void);
30-
bool rb_yjit_threshold_hit(const rb_iseq_t *const iseq);
30+
bool rb_yjit_threshold_hit(const rb_iseq_t *const iseq, unsigned long total_calls);
3131
void rb_yjit_invalidate_all_method_lookup_assumptions(void);
3232
void rb_yjit_cme_invalidate(rb_callable_method_entry_t *cme);
3333
void rb_yjit_collect_binding_alloc(void);
3434
void rb_yjit_collect_binding_set(void);
35-
bool rb_yjit_compile_iseq(const rb_iseq_t *iseq, rb_execution_context_t *ec);
35+
void rb_yjit_compile_iseq(const rb_iseq_t *iseq, rb_execution_context_t *ec, bool jit_exception);
3636
void rb_yjit_init(void);
3737
void rb_yjit_bop_redefined(int redefined_flag, enum ruby_basic_operators bop);
3838
void rb_yjit_constant_state_changed(ID id);
@@ -49,12 +49,12 @@ void rb_yjit_tracing_invalidate_all(void);
4949

5050
static inline bool rb_yjit_enabled_p(void) { return false; }
5151
static inline bool rb_yjit_compile_new_iseqs(void) { return false; }
52-
static inline bool rb_yjit_threshold_hit(const rb_iseq_t *const iseq) { return false; }
52+
static inline bool rb_yjit_threshold_hit(const rb_iseq_t *const iseq, unsigned long total_calls) { return false; }
5353
static inline void rb_yjit_invalidate_all_method_lookup_assumptions(void) {}
5454
static inline void rb_yjit_cme_invalidate(rb_callable_method_entry_t *cme) {}
5555
static inline void rb_yjit_collect_binding_alloc(void) {}
5656
static inline void rb_yjit_collect_binding_set(void) {}
57-
static inline bool rb_yjit_compile_iseq(const rb_iseq_t *iseq, rb_execution_context_t *ec) { return false; }
57+
static inline void rb_yjit_compile_iseq(const rb_iseq_t *iseq, rb_execution_context_t *ec, bool jit_exception) {}
5858
static inline void rb_yjit_init(void) {}
5959
static inline void rb_yjit_bop_redefined(int redefined_flag, enum ruby_basic_operators bop) {}
6060
static inline void rb_yjit_constant_state_changed(ID id) {}

0 commit comments

Comments
 (0)