diff options
-rw-r--r-- | mjit.c | 391 | ||||
-rw-r--r-- | mjit_c.h | 24 | ||||
-rw-r--r-- | mjit_c.rb | 7 | ||||
-rw-r--r-- | test/lib/jit_support.rb | 2 | ||||
-rw-r--r-- | test/ruby/test_mjit.rb | 269 | ||||
-rw-r--r-- | test/ruby/test_rubyvm_mjit.rb | 23 |
6 files changed, 330 insertions, 386 deletions
@@ -124,12 +124,6 @@ #define MJIT_TMP_PREFIX "_ruby_mjit_" -// Linked list of struct rb_mjit_unit. -struct rb_mjit_unit_list { - struct ccan_list_head head; - int length; // the list length -}; - extern void rb_native_mutex_lock(rb_nativethread_lock_t *lock); extern void rb_native_mutex_unlock(rb_nativethread_lock_t *lock); extern void rb_native_mutex_initialize(rb_nativethread_lock_t *lock); @@ -148,6 +142,11 @@ bool mjit_enabled = false; // true if JIT-ed code should be called. When `ruby_vm_event_enabled_global_flags & ISEQ_TRACE_EVENTS` // and `mjit_call_p == false`, any JIT-ed code execution is cancelled as soon as possible. bool mjit_call_p = false; +// There's an ISEQ in unit_queue whose total_calls reached 2 * call_threshold. +// If this is true, check_unit_queue will start compiling ISEQs in unit_queue. +static bool mjit_compile_p = false; +// The actual number of units in active_units +static int active_units_length = 0; // Priority queue of iseqs waiting for JIT compilation. // This variable is a pointer to head unit of the queue. @@ -162,10 +161,6 @@ static struct rb_mjit_unit_list stale_units = { CCAN_LIST_HEAD_INIT(stale_units. static int current_unit_num; // A mutex for conitionals and critical sections. static rb_nativethread_lock_t mjit_engine_mutex; -// The times when unload_units is requested. unload_units is called after some requests. -static int unload_requests = 0; -// The total number of unloaded units. -static int total_unloads = 0; // Set to true to stop worker. static bool stop_worker_p; // Set to true if worker is stopped. @@ -662,10 +657,17 @@ c_compile_unit(struct rb_mjit_unit *unit) static void compile_prelude(FILE *f); -// Compile all JIT code into a single .c file static bool -mjit_compact(char* c_file) +mjit_batch(struct rb_mjit_unit *unit) { + VM_ASSERT(unit->type == MJIT_UNIT_BATCH); + static const char c_ext[] = ".c"; + static const char so_ext[] = DLEXT; + char c_file[MAXPATHLEN], so_file[MAXPATHLEN]; + + sprint_uniq_filename(c_file, (int)sizeof(c_file), unit->id, MJIT_TMP_PREFIX, c_ext); + sprint_uniq_filename(so_file, (int)sizeof(so_file), unit->id, MJIT_TMP_PREFIX, so_ext); + FILE *f; int fd = rb_cloexec_open(c_file, c_file_access_mode, 0600); if (fd < 0 || (f = fdopen(fd, "w")) == NULL) { @@ -677,18 +679,11 @@ mjit_compact(char* c_file) compile_prelude(f); - // This entire loop lock GC so that we do not need to consider a case that - // ISeq is GC-ed in a middle of re-compilation. It takes 3~4ms with 100 methods - // on my machine. It's not too bad compared to compilation time of C (7200~8000ms), - // but it might be larger if we use a larger --jit-max-cache. - // - // TODO: Consider using a more granular lock after we implement inlining across - // compacted functions (not done yet). bool success = true; struct rb_mjit_unit *child_unit = 0; - ccan_list_for_each(&active_units.head, child_unit, unode) { + ccan_list_for_each(&unit->units.head, child_unit, unode) { if (!success) continue; - if (ISEQ_BODY(child_unit->iseq)->mjit_unit == NULL) continue; // Sometimes such units are created. TODO: Investigate why + if (child_unit->iseq == NULL) continue; // ISEQ is GCed char funcname[MAXPATHLEN]; sprint_funcname(funcname, sizeof(funcname), child_unit); @@ -709,8 +704,9 @@ mjit_compact(char* c_file) // Compile all cached .c files and build a single .so file. Reload all JIT func from it. // This improves the code locality for better performance in terms of iTLB and iCache. static bool -mjit_compact_unit(struct rb_mjit_unit *unit) +mjit_compact(struct rb_mjit_unit *unit) { + VM_ASSERT(unit->type == MJIT_UNIT_COMPACT); static const char c_ext[] = ".c"; static const char so_ext[] = DLEXT; char c_file[MAXPATHLEN], so_file[MAXPATHLEN]; @@ -718,61 +714,128 @@ mjit_compact_unit(struct rb_mjit_unit *unit) sprint_uniq_filename(c_file, (int)sizeof(c_file), unit->id, MJIT_TMP_PREFIX, c_ext); sprint_uniq_filename(so_file, (int)sizeof(so_file), unit->id, MJIT_TMP_PREFIX, so_ext); - return mjit_compact(c_file); + FILE *f; + int fd = rb_cloexec_open(c_file, c_file_access_mode, 0600); + if (fd < 0 || (f = fdopen(fd, "w")) == NULL) { + int e = errno; + if (fd >= 0) (void)close(fd); + verbose(1, "Failed to fopen '%s', giving up JIT for it (%s)", c_file, strerror(e)); + return false; + } + + compile_prelude(f); + + bool success = true; + struct rb_mjit_unit *batch_unit = 0, *child_unit = 0; + ccan_list_for_each(&active_units.head, batch_unit, unode) { + ccan_list_for_each(&batch_unit->units.head, child_unit, unode) { + if (!success) continue; + if (child_unit->iseq == NULL) continue; // ISEQ is GCed + + char funcname[MAXPATHLEN]; + sprint_funcname(funcname, sizeof(funcname), child_unit); + + int iseq_lineno = ISEQ_BODY(child_unit->iseq)->location.first_lineno; + const char *sep = "@"; + const char *iseq_label = RSTRING_PTR(ISEQ_BODY(child_unit->iseq)->location.label); + const char *iseq_path = RSTRING_PTR(rb_iseq_path(child_unit->iseq)); + if (!iseq_label) iseq_label = sep = ""; + fprintf(f, "\n/* %s%s%s:%d */\n", iseq_label, sep, iseq_path, iseq_lineno); + success &= mjit_compile(f, child_unit->iseq, funcname, child_unit->id); + } + } + + fclose(f); + return success; } static void -load_compact_funcs_from_so(struct rb_mjit_unit *unit, char *c_file, char *so_file) +load_batch_funcs_from_so(struct rb_mjit_unit *unit, char *c_file, char *so_file) { - struct rb_mjit_unit *cur = 0; double end_time = real_ms_time(); void *handle = dlopen(so_file, RTLD_NOW); if (handle == NULL) { - mjit_warning("failure in loading code from compacted '%s': %s", so_file, dlerror()); + mjit_warning("failure in loading code from batched '%s': %s", so_file, dlerror()); xfree(unit); return; } unit->handle = handle; - // lazily dlclose handle (and .so file for win32) on `mjit_finish()`. - add_to_list(unit, &compact_units); + // lazily dlclose handle on `mjit_finish()`. + add_to_list(unit, &active_units); + active_units_length += unit->units.length; if (!mjit_opts.save_temps) remove_so_file(so_file, unit); - ccan_list_for_each(&active_units.head, cur, unode) { - void *func; + struct rb_mjit_unit *child_unit = 0; + ccan_list_for_each(&unit->units.head, child_unit, unode) { char funcname[MAXPATHLEN]; - sprint_funcname(funcname, sizeof(funcname), cur); + sprint_funcname(funcname, sizeof(funcname), child_unit); + void *func; if ((func = dlsym(handle, funcname)) == NULL) { - mjit_warning("skipping to reload '%s' from '%s': %s", funcname, so_file, dlerror()); + mjit_warning("skipping to load '%s' from '%s': %s", funcname, so_file, dlerror()); continue; } - if (cur->iseq) { // Check whether GCed or not + if (child_unit->iseq) { // Check whether GCed or not // Usage of jit_code might be not in a critical section. - MJIT_ATOMIC_SET(ISEQ_BODY(cur->iseq)->jit_func, (jit_func_t)func); + const rb_iseq_t *iseq = child_unit->iseq; + MJIT_ATOMIC_SET(ISEQ_BODY(iseq)->jit_func, (jit_func_t)func); + + verbose(1, "JIT success: %s@%s:%d", + RSTRING_PTR(ISEQ_BODY(iseq)->location.label), + RSTRING_PTR(rb_iseq_path(iseq)), ISEQ_BODY(iseq)->location.first_lineno); + } + else { + verbose(1, "JIT skip: A compiled method has been GCed"); } } - verbose(1, "JIT compaction (%.1fms): Compacted %d methods %s -> %s", end_time - current_cc_ms, active_units.length, c_file, so_file); + verbose(1, "JIT batch (%.1fms): Batched %d methods %s -> %s", end_time - current_cc_ms, unit->units.length, c_file, so_file); } -static void * -load_func_from_so(const char *so_file, const char *funcname, struct rb_mjit_unit *unit) +static void +load_compact_funcs_from_so(struct rb_mjit_unit *unit, char *c_file, char *so_file) { - void *handle, *func; + double end_time = real_ms_time(); - handle = dlopen(so_file, RTLD_NOW); + void *handle = dlopen(so_file, RTLD_NOW); if (handle == NULL) { - mjit_warning("failure in loading code from '%s': %s", so_file, dlerror()); - return (void *)MJIT_FUNC_FAILED; + mjit_warning("failure in loading code from compacted '%s': %s", so_file, dlerror()); + xfree(unit); + return; } - - func = dlsym(handle, funcname); unit->handle = handle; - return func; + + // lazily dlclose handle on `mjit_finish()`. + add_to_list(unit, &compact_units); + + if (!mjit_opts.save_temps) + remove_so_file(so_file, unit); + + struct rb_mjit_unit *batch_unit = 0, *child_unit = 0; + ccan_list_for_each(&active_units.head, batch_unit, unode) { + ccan_list_for_each(&batch_unit->units.head, child_unit, unode) { + if (child_unit->iseq == NULL) continue; // ISEQ is GCed + + char funcname[MAXPATHLEN]; + sprint_funcname(funcname, sizeof(funcname), child_unit); + + void *func; + if ((func = dlsym(handle, funcname)) == NULL) { + mjit_warning("skipping to reload '%s' from '%s': %s", funcname, so_file, dlerror()); + continue; + } + + if (child_unit->iseq) { // Check whether GCed or not + // Usage of jit_code might be not in a critical section. + MJIT_ATOMIC_SET(ISEQ_BODY(child_unit->iseq)->jit_func, (jit_func_t)func); + } + } + } + verbose(1, "JIT compaction (%.1fms): Compacted %d methods %s -> %s", end_time - current_cc_ms, active_units_length, c_file, so_file); } #ifndef __clang__ @@ -813,51 +876,6 @@ compile_prelude(FILE *f) #endif } -// Compile ISeq in UNIT and return function pointer of JIT-ed code. -// It may return MJIT_FUNC_FAILED if something went wrong. -static bool -mjit_compile_unit(struct rb_mjit_unit *unit) -{ - static const char c_ext[] = ".c"; - static const char so_ext[] = DLEXT; - char c_file[MAXPATHLEN], so_file[MAXPATHLEN], funcname[MAXPATHLEN]; - - sprint_uniq_filename(c_file, (int)sizeof(c_file), unit->id, MJIT_TMP_PREFIX, c_ext); - sprint_uniq_filename(so_file, (int)sizeof(so_file), unit->id, MJIT_TMP_PREFIX, so_ext); - sprint_funcname(funcname, sizeof(funcname), unit); - - FILE *f; - int fd = rb_cloexec_open(c_file, c_file_access_mode, 0600); - if (fd < 0 || (f = fdopen(fd, "w")) == NULL) { - int e = errno; - if (fd >= 0) (void)close(fd); - verbose(1, "Failed to fopen '%s', giving up JIT for it (%s)", c_file, strerror(e)); - return false; - } - - // print #include of MJIT header, etc. - compile_prelude(f); - - // To make MJIT worker thread-safe against GC.compact, copy ISeq values while `in_jit` is true. - int iseq_lineno = ISEQ_BODY(unit->iseq)->location.first_lineno; - char *iseq_label = alloca(RSTRING_LEN(ISEQ_BODY(unit->iseq)->location.label) + 1); - char *iseq_path = alloca(RSTRING_LEN(rb_iseq_path(unit->iseq)) + 1); - strcpy(iseq_label, RSTRING_PTR(ISEQ_BODY(unit->iseq)->location.label)); - strcpy(iseq_path, RSTRING_PTR(rb_iseq_path(unit->iseq))); - - verbose(2, "start compilation: %s@%s:%d -> %s", iseq_label, iseq_path, iseq_lineno, c_file); - fprintf(f, "/* %s@%s:%d */\n\n", iseq_label, iseq_path, iseq_lineno); - bool success = mjit_compile(f, unit->iseq, funcname, unit->id); - - fclose(f); - if (!success) { - if (!mjit_opts.save_temps) - remove_file(c_file); - verbose(1, "JIT failure: %s@%s:%d -> %s", iseq_label, iseq_path, iseq_lineno, c_file); - } - return success; -} - static pid_t start_c_compile_unit(struct rb_mjit_unit *unit) { @@ -915,79 +933,6 @@ mjit_capture_cc_entries(const struct rb_iseq_constant_body *compiled_iseq, const return cc_entries_index; } -// Set up field `used_code_p` for unit iseqs whose iseq on the stack of ec. -static void -mark_iseq_units(const rb_iseq_t *iseq, void *data) -{ - if (ISEQ_BODY(iseq)->mjit_unit != NULL) { - ISEQ_BODY(iseq)->mjit_unit->used_code_p = true; - } -} - -// Unload JIT code of some units to satisfy the maximum permitted -// number of units with a loaded code. -static void -unload_units(void) -{ - struct rb_mjit_unit *unit = 0, *next; - int units_num = active_units.length; - - // For now, we don't unload units when ISeq is GCed. We should - // unload such ISeqs first here. - ccan_list_for_each_safe(&active_units.head, unit, next, unode) { - if (unit->iseq == NULL) { // ISeq is GCed. - remove_from_list(unit, &active_units); - free_unit(unit); - } - } - - // Detect units which are in use and can't be unloaded. - ccan_list_for_each(&active_units.head, unit, unode) { - VM_ASSERT(unit->iseq != NULL && unit->handle != NULL); - unit->used_code_p = false; - } - // All threads have a root_fiber which has a mjit_cont. Other normal fibers also - // have a mjit_cont. Thus we can check ISeqs in use by scanning ec of mjit_conts. - rb_jit_cont_each_iseq(mark_iseq_units, NULL); - // TODO: check stale_units and unload unused ones! (note that the unit is not associated to ISeq anymore) - - // Unload units whose total_calls is smaller than any total_calls in unit_queue. - // TODO: make the algorithm more efficient - long unsigned prev_queue_calls = -1; - while (true) { - // Calculate the next max total_calls in unit_queue - long unsigned max_queue_calls = 0; - ccan_list_for_each(&unit_queue.head, unit, unode) { - if (unit->iseq != NULL && max_queue_calls < ISEQ_BODY(unit->iseq)->total_calls - && ISEQ_BODY(unit->iseq)->total_calls < prev_queue_calls) { - max_queue_calls = ISEQ_BODY(unit->iseq)->total_calls; - } - } - prev_queue_calls = max_queue_calls; - - bool unloaded_p = false; - ccan_list_for_each_safe(&active_units.head, unit, next, unode) { - if (unit->used_code_p) // We can't unload code on stack. - continue; - - if (max_queue_calls > ISEQ_BODY(unit->iseq)->total_calls) { - verbose(2, "Unloading unit %d (calls=%lu, threshold=%lu)", - unit->id, ISEQ_BODY(unit->iseq)->total_calls, max_queue_calls); - VM_ASSERT(unit->handle != NULL); - remove_from_list(unit, &active_units); - free_unit(unit); - unloaded_p = true; - } - } - if (!unloaded_p) break; - } - - if (units_num > active_units.length) { - verbose(1, "Too many JIT code -- %d units unloaded", units_num - active_units.length); - total_unloads += units_num - active_units.length; - } -} - static void mjit_add_iseq_to_process(const rb_iseq_t *iseq, const struct rb_mjit_compile_info *compile_info); // Return an unique file name in /tmp with PREFIX and SUFFIX and @@ -1110,6 +1055,9 @@ create_unit(enum rb_mjit_unit_type type) struct rb_mjit_unit *unit = ZALLOC_N(struct rb_mjit_unit, 1); unit->id = current_unit_num++; unit->type = type; + if (type == MJIT_UNIT_BATCH) { + ccan_list_head_init(&unit->units.head); + } return unit; } @@ -1132,28 +1080,26 @@ check_unit_queue(void) if (worker_stopped) return; if (current_cc_pid != 0) return; // still compiling - // Run unload_units after it's requested `max_cache_size / 10` (default: 10) times. - // This throttles the call to mitigate locking in unload_units. It also throttles JIT compaction. - int throttle_threshold = mjit_opts.max_cache_size / 10; - if (unload_requests >= throttle_threshold) { - unload_units(); - unload_requests = 0; - if (active_units.length == mjit_opts.max_cache_size && mjit_opts.wait) { // Sometimes all methods may be in use - mjit_opts.max_cache_size++; // avoid infinite loop on `mjit_wait`. Note that --jit-wait is just for testing. - verbose(1, "No units can be unloaded -- incremented max-cache-size to %d for --jit-wait", mjit_opts.max_cache_size); - } - } - if (active_units.length >= mjit_opts.max_cache_size) return; // wait until unload_units makes a progress + // TODO: resurrect unload_units + if (active_units_length >= mjit_opts.max_cache_size) return; // wait until unload_units makes a progress - // Dequeue a unit - struct rb_mjit_unit *unit = get_from_list(&unit_queue); - if (unit == NULL) return; - VM_ASSERT(unit->type == MJIT_UNIT_ISEQ); + // No ISEQ in unit_queue has enough calls to trigger JIT + if (!mjit_compile_p) return; + mjit_compile_p = false; + + // Compile all ISEQs in unit_queue together + struct rb_mjit_unit *unit = create_unit(MJIT_UNIT_BATCH); + struct rb_mjit_unit *child_unit = NULL; + VM_ASSERT(unit_queue.length > 0); + while ((child_unit = get_from_list(&unit_queue)) != NULL && (active_units_length + unit->units.length) < mjit_opts.max_cache_size) { + add_to_list(child_unit, &unit->units); + ISEQ_BODY(child_unit->iseq)->jit_func = (jit_func_t)MJIT_FUNC_COMPILING; + } // Run the MJIT compiler synchronously current_cc_ms = real_ms_time(); current_cc_unit = unit; - bool success = mjit_compile_unit(unit); + bool success = mjit_batch(unit); if (!success) { mjit_notify_waitpid(1); return; @@ -1181,19 +1127,13 @@ check_compaction(void) int max_compact_size = mjit_opts.max_cache_size / 100; if (max_compact_size < 10) max_compact_size = 10; - // Run unload_units after it's requested `max_cache_size / 10` (default: 10) times. - // This throttles the call to mitigate locking in unload_units. It also throttles JIT compaction. - int throttle_threshold = mjit_opts.max_cache_size / 10; - - if (compact_units.length < max_compact_size - && ((!mjit_opts.wait && unit_queue.length == 0 && active_units.length > 1) - || (active_units.length == mjit_opts.max_cache_size && compact_units.length * throttle_threshold <= total_unloads))) { // throttle compaction by total_unloads + if (active_units_length == mjit_opts.max_cache_size) { struct rb_mjit_unit *unit = create_unit(MJIT_UNIT_COMPACT); // Run the MJIT compiler synchronously current_cc_ms = real_ms_time(); current_cc_unit = unit; - bool success = mjit_compact_unit(unit); + bool success = mjit_compact(unit); if (!success) { mjit_notify_waitpid(1); return; @@ -1227,9 +1167,8 @@ mjit_notify_waitpid(int exit_code) // Check the result if (exit_code != 0) { verbose(2, "Failed to generate so"); - if (current_cc_unit->type == MJIT_UNIT_ISEQ) { - current_cc_unit->iseq->body->jit_func = (jit_func_t)MJIT_FUNC_FAILED; - } + // TODO: set MJIT_FUNC_FAILED to unit->units + // TODO: free list of unit->units free_unit(current_cc_unit); current_cc_unit = NULL; return; @@ -1238,39 +1177,22 @@ mjit_notify_waitpid(int exit_code) // Load .so file char so_file[MAXPATHLEN]; sprint_uniq_filename(so_file, (int)sizeof(so_file), current_cc_unit->id, MJIT_TMP_PREFIX, DLEXT); - if (current_cc_unit->type == MJIT_UNIT_COMPACT) { - load_compact_funcs_from_so(current_cc_unit, c_file, so_file); - current_cc_unit = NULL; - } - else { // MJIT_UNIT_ISEQ - // Load the function from so - char funcname[MAXPATHLEN]; - sprint_funcname(funcname, sizeof(funcname), current_cc_unit); - void *func = load_func_from_so(so_file, funcname, current_cc_unit); - - // Delete .so file - if (!mjit_opts.save_temps) - remove_file(so_file); - - // Set the jit_func if successful - if (current_cc_unit->iseq != NULL) { // mjit_free_iseq could nullify this - rb_iseq_t *iseq = current_cc_unit->iseq; - if (!MJIT_FUNC_STATE_P(func)) { - double end_time = real_ms_time(); - verbose(1, "JIT success (%.1fms): %s@%s:%d -> %s", - end_time - current_cc_ms, RSTRING_PTR(ISEQ_BODY(iseq)->location.label), - RSTRING_PTR(rb_iseq_path(iseq)), ISEQ_BODY(iseq)->location.first_lineno, c_file); - - add_to_list(current_cc_unit, &active_units); - } - MJIT_ATOMIC_SET(ISEQ_BODY(iseq)->jit_func, func); - } // TODO: free unit on else? + switch (current_cc_unit->type) { + case MJIT_UNIT_ISEQ: + rb_bug("unreachable: current_cc_unit->type must not be MJIT_UNIT_ISEQ"); + case MJIT_UNIT_BATCH: + load_batch_funcs_from_so(current_cc_unit, c_file, so_file); current_cc_unit = NULL; // Run compaction if it should if (!stop_worker_p) { check_compaction(); } + break; + case MJIT_UNIT_COMPACT: + load_compact_funcs_from_so(current_cc_unit, c_file, so_file); + current_cc_unit = NULL; + break; } // Skip further compilation if mjit_finish is trying to stop it @@ -1328,13 +1250,29 @@ mjit_add_iseq_to_process(const rb_iseq_t *iseq, const struct rb_mjit_compile_inf return; } - ISEQ_BODY(iseq)->jit_func = (jit_func_t)MJIT_FUNC_COMPILING; - create_iseq_unit(iseq); - if (compile_info != NULL) - ISEQ_BODY(iseq)->mjit_unit->compile_info = *compile_info; - add_to_list(ISEQ_BODY(iseq)->mjit_unit, &unit_queue); - if (active_units.length >= mjit_opts.max_cache_size) { - unload_requests++; + // For batching multiple ISEQs, we only enqueue ISEQs when total_calls reaches call_threshold, + // and compile all enqueued ISEQs when any ISEQ reaches call_threshold * 2. + bool recompile_p = !MJIT_FUNC_STATE_P(ISEQ_BODY(iseq)->jit_func); + if (!ISEQ_BODY(iseq)->mjit_unit || recompile_p) { // call_threshold, or recompile + // Discard an old unit with recompile_p + if (recompile_p) { + ISEQ_BODY(iseq)->mjit_unit->iseq = NULL; // Ignore this from compaction + ISEQ_BODY(iseq)->jit_func = (jit_func_t)MJIT_FUNC_NOT_COMPILED; + active_units_length--; + } + + // Create a new unit and enqueue it + struct rb_mjit_unit *unit = create_iseq_unit(iseq); + if (recompile_p) { + VM_ASSERT(compile_info != NULL); + unit->compile_info = *compile_info; + } + add_to_list(unit, &unit_queue); + ISEQ_BODY(iseq)->total_calls = 0; // come here again :) + } + else { // call_threshold * 2 + VM_ASSERT(compile_info == NULL); + mjit_compile_p = true; // compile all ISEQs in unit_queue } } @@ -1715,6 +1653,11 @@ mjit_init(const struct mjit_options *opts) // Normalize options if (mjit_opts.call_threshold == 0) mjit_opts.call_threshold = DEFAULT_CALL_THRESHOLD; + if (mjit_opts.call_threshold % 2 == 1) { + mjit_opts.call_threshold += 1; + mjit_warning("--mjit-call-threshold must be an even number. Using %d instead.", mjit_opts.call_threshold); + } + mjit_opts.call_threshold /= 2; // Half for enqueue, half for trigger if (mjit_opts.max_cache_size <= 0) mjit_opts.max_cache_size = DEFAULT_MAX_CACHE_SIZE; if (mjit_opts.max_cache_size < MIN_CACHE_SIZE) @@ -14,25 +14,31 @@ #define NOT_COMPILED_STACK_SIZE -1 #define ALREADY_COMPILED_P(status, pos) (status->stack_size_for_pos[pos] != NOT_COMPILED_STACK_SIZE) -// Type of rb_mjit_unit +// Linked list of struct rb_mjit_unit. +struct rb_mjit_unit_list { + struct ccan_list_head head; + int length; // the list length +}; + enum rb_mjit_unit_type { - // Single-ISEQ unit for mjit_compile + // Single-ISEQ unit for unit_queue MJIT_UNIT_ISEQ = 0, + // Multi-ISEQ unit for mjit_batch + MJIT_UNIT_BATCH = 1, // All-ISEQ unit for mjit_compact - MJIT_UNIT_COMPACT = 1, + MJIT_UNIT_COMPACT = 2, }; // The unit structure that holds metadata of ISeq for MJIT. -// TODO: Use different structs for ISEQ and COMPACT +// TODO: Use different structs for ISEQ and BATCH/COMPACT struct rb_mjit_unit { struct ccan_list_node unode; // Unique order number of unit. int id; - // Dlopen handle of the loaded object file. - void *handle; // Type of this unit enum rb_mjit_unit_type type; + /* MJIT_UNIT_ISEQ */ // ISEQ for a non-batch unit rb_iseq_t *iseq; // Only used by unload_units. Flag to check this unit is currently on stack or not. @@ -43,6 +49,12 @@ struct rb_mjit_unit { const struct rb_callcache **cc_entries; // ISEQ_BODY(iseq)->ci_size + ones of inlined iseqs unsigned int cc_entries_size; + + /* MJIT_UNIT_BATCH, MJIT_UNIT_COMPACT */ + // Dlopen handle of the loaded object file. + void *handle; + // Units compacted by this batch + struct rb_mjit_unit_list units; // MJIT_UNIT_BATCH only }; // Storage to keep data which is consistent in each conditional branch. @@ -620,13 +620,14 @@ module RubyVM::MJIT "rb_mjit_unit", Primitive.cexpr!("SIZEOF(struct rb_mjit_unit)"), unode: [self.ccan_list_node, Primitive.cexpr!("OFFSETOF((*((struct rb_mjit_unit *)NULL)), unode)")], id: [CType::Immediate.parse("int"), Primitive.cexpr!("OFFSETOF((*((struct rb_mjit_unit *)NULL)), id)")], - handle: [CType::Pointer.new { CType::Immediate.parse("void") }, Primitive.cexpr!("OFFSETOF((*((struct rb_mjit_unit *)NULL)), handle)")], type: [self.rb_mjit_unit_type, Primitive.cexpr!("OFFSETOF((*((struct rb_mjit_unit *)NULL)), type)")], iseq: [CType::Pointer.new { self.rb_iseq_t }, Primitive.cexpr!("OFFSETOF((*((struct rb_mjit_unit *)NULL)), iseq)")], used_code_p: [self._Bool, Primitive.cexpr!("OFFSETOF((*((struct rb_mjit_unit *)NULL)), used_code_p)")], compile_info: [self.rb_mjit_compile_info, Primitive.cexpr!("OFFSETOF((*((struct rb_mjit_unit *)NULL)), compile_info)")], cc_entries: [CType::Pointer.new { CType::Pointer.new { self.rb_callcache } }, Primitive.cexpr!("OFFSETOF((*((struct rb_mjit_unit *)NULL)), cc_entries)")], cc_entries_size: [CType::Immediate.parse("unsigned int"), Primitive.cexpr!("OFFSETOF((*((struct rb_mjit_unit *)NULL)), cc_entries_size)")], + handle: [CType::Pointer.new { CType::Immediate.parse("void") }, Primitive.cexpr!("OFFSETOF((*((struct rb_mjit_unit *)NULL)), handle)")], + units: [self.rb_mjit_unit_list, Primitive.cexpr!("OFFSETOF((*((struct rb_mjit_unit *)NULL)), units)")], ) end @@ -787,5 +788,9 @@ module RubyVM::MJIT CType::Stub.new(:rb_mjit_unit_type) end + def C.rb_mjit_unit_list + CType::Stub.new(:rb_mjit_unit_list) + end + ### MJIT bindgen end ### end if RubyVM::MJIT.enabled? && RubyVM::MJIT.const_defined?(:C) # not defined for miniruby diff --git a/test/lib/jit_support.rb b/test/lib/jit_support.rb index b75b0b8847..812c13b925 100644 --- a/test/lib/jit_support.rb +++ b/test/lib/jit_support.rb @@ -2,7 +2,7 @@ require 'rbconfig' module JITSupport JIT_TIMEOUT = 600 # 10min for each... - JIT_SUCCESS_PREFIX = 'JIT success \(\d+\.\dms\)' + JIT_SUCCESS_PREFIX = 'JIT success' JIT_RECOMPILE_PREFIX = 'JIT recompile' JIT_COMPACTION_PREFIX = 'JIT compaction \(\d+\.\dms\)' UNSUPPORTED_COMPILERS = [ diff --git a/test/ruby/test_mjit.rb b/test/ruby/test_mjit.rb index efdf4e9606..3120d7e780 100644 --- a/test/ruby/test_mjit.rb +++ b/test/ruby/test_mjit.rb @@ -11,6 +11,7 @@ class TestMJIT < Test::Unit::TestCase /\AJIT recompile: .+\n\z/, /\AJIT inline: .+\n\z/, /\AJIT cancel: .+\n\z/, + /\AJIT batch \([^)]+ms\): .+\n\z/, /\ASuccessful MJIT finish\n\z/, ] MAX_CACHE_PATTERNS = [ @@ -67,11 +68,11 @@ class TestMJIT < Test::Unit::TestCase end def test_compile_insn_nop - assert_compile_once('nil rescue true', result_inspect: 'nil', insns: %i[nop]) + assert_compile_twice('nil rescue true', result_inspect: 'nil', insns: %i[nop]) end def test_compile_insn_local - assert_compile_once("#{<<~"begin;"}\n#{<<~"end;"}", result_inspect: '1', insns: %i[setlocal_WC_0 getlocal_WC_0]) + assert_compile_twice("#{<<~"begin;"}\n#{<<~"end;"}", result_inspect: '1', insns: %i[setlocal_WC_0 getlocal_WC_0]) begin; foo = 1 foo @@ -96,7 +97,7 @@ class TestMJIT < Test::Unit::TestCase end def test_compile_insn_blockparam - assert_eval_with_jit("#{<<~"begin;"}\n#{<<~"end;"}", stdout: '3', success_count: 2, insns: %i[getblockparam setblockparam]) + assert_eval_with_jit("#{<<~"begin;"}\n#{<<~"end;"}", stdout: '33', success_count: 2, insns: %i[getblockparam setblockparam]) begin; def foo(&b) a = b @@ -105,6 +106,7 @@ class TestMJIT < Test::Unit::TestCase end print foo { 1 } + print foo { 1 } end; end @@ -124,25 +126,25 @@ class TestMJIT < Test::Unit::TestCase end def test_compile_insn_getspecial - assert_compile_once('$1', result_inspect: 'nil', insns: %i[getspecial]) + assert_compile_twice('$1', result_inspect: 'nil', insns: %i[getspecial]) end def test_compile_insn_setspecial - assert_compile_once("#{<<~"begin;"}\n#{<<~"end;"}", result_inspect: 'true', insns: %i[setspecial]) + assert_compile_twice("#{<<~"begin;"}\n#{<<~"end;"}", result_inspect: 'true', insns: %i[setspecial]) begin; true if nil.nil?..nil.nil? end; end def test_compile_insn_instancevariable - assert_compile_once("#{<<~"begin;"}\n#{<<~"end;"}", result_inspect: '1', insns: %i[getinstancevariable setinstancevariable]) + assert_compile_twice("#{<<~"begin;"}\n#{<<~"end;"}", result_inspect: '1', insns: %i[getinstancevariable setinstancevariable]) begin; @foo = 1 @foo end; # optimized getinstancevariable call - assert_eval_with_jit("#{<<~"begin;"}\n#{<<~"end;"}", stdout: '33', success_count: 1, call_threshold: 2) + assert_eval_with_jit("#{<<~"begin;"}\n#{<<~"end;"}", stdout: '33', success_count: 2, call_threshold: 2) begin; class A def initialize @@ -162,7 +164,7 @@ class TestMJIT < Test::Unit::TestCase end def test_compile_insn_classvariable - assert_eval_with_jit("#{<<~"begin;"}\n#{<<~"end;"}", stdout: '1', success_count: 1, insns: %i[getclassvariable setclassvariable]) + assert_eval_with_jit("#{<<~"begin;"}\n#{<<~"end;"}", stdout: '11', success_count: 1, insns: %i[getclassvariable setclassvariable]) begin; class Foo def self.foo @@ -172,19 +174,22 @@ class TestMJIT < Test::Unit::TestCase end print Foo.foo + print Foo.foo end; end def test_compile_insn_constant - assert_compile_once("#{<<~"begin;"}\n#{<<~"end;"}", result_inspect: '1', insns: %i[opt_getconstant_path setconstant]) - begin; - FOO = 1 - FOO - end; + EnvUtil.suppress_warning do + assert_compile_twice("#{<<~"begin;"}\n#{<<~"end;"}", result_inspect: '1', insns: %i[opt_getconstant_path setconstant]) + begin; + FOO = 1 + FOO + end; + end end def test_compile_insn_global - assert_compile_once("#{<<~"begin;"}\n#{<<~"end;"}", result_inspect: '1', insns: %i[getglobal setglobal]) + assert_compile_twice("#{<<~"begin;"}\n#{<<~"end;"}", result_inspect: '1', insns: %i[getglobal setglobal]) begin; $foo = 1 $foo @@ -192,26 +197,26 @@ class TestMJIT < Test::Unit::TestCase end def test_compile_insn_putnil - assert_compile_once('nil', result_inspect: 'nil', insns: %i[putnil]) + assert_compile_twice('nil', result_inspect: 'nil', insns: %i[putnil]) end def test_compile_insn_putself - assert_eval_with_jit("#{<<~"begin;"}\n#{<<~"end;"}", stdout: 'hello', success_count: 1, insns: %i[putself]) + assert_eval_with_jit("#{<<~"begin;"}\n#{<<~"end;"}", stdout: 'hellohello', success_count: 1, insns: %i[putself]) begin; - proc { print "hello" }.call + 2.times { print "hello" } end; end def test_compile_insn_putobject - assert_compile_once('0', result_inspect: '0', insns: %i[putobject_INT2FIX_0_]) - assert_compile_once('1', result_inspect: '1', insns: %i[putobject_INT2FIX_1_]) - assert_compile_once('2', result_inspect: '2', insns: %i[putobject]) + assert_compile_twice('0', result_inspect: '0', insns: %i[putobject_INT2FIX_0_]) + assert_compile_twice('1', result_inspect: '1', insns: %i[putobject_INT2FIX_1_]) + assert_compile_twice('2', result_inspect: '2', insns: %i[putobject]) end def test_compile_insn_definemethod_definesmethod - assert_eval_with_jit("#{<<~"begin;"}\n#{<<~"end;"}", stdout: 'helloworld', success_count: 3, insns: %i[definemethod definesmethod]) + assert_eval_with_jit("#{<<~"begin;"}\n#{<<~"end;"}", stdout: 'helloworldhelloworld', success_count: 3, insns: %i[definemethod definesmethod]) begin; - print 1.times.map { + print 2.times.map { def method_definition 'hello' end @@ -226,9 +231,9 @@ class TestMJIT < Test::Unit::TestCase end def test_compile_insn_putspecialobject - assert_eval_with_jit("#{<<~"begin;"}\n#{<<~"end;"}", stdout: 'a', success_count: 2, insns: %i[putspecialobject]) + assert_eval_with_jit("#{<<~"begin;"}\n#{<<~"end;"}", stdout: 'aa', success_count: 2, insns: %i[putspecialobject]) begin; - print 1.times.map { + print 2.times.map { def a 'a' end @@ -241,15 +246,15 @@ class TestMJIT < Test::Unit::TestCase end def test_compile_insn_putstring_concatstrings_objtostring - assert_compile_once('"a#{}b" + "c"', result_inspect: '"abc"', insns: %i[putstring concatstrings objtostring]) + assert_compile_twice('"a#{}b" + "c"', result_inspect: '"abc"', insns: %i[putstring concatstrings objtostring]) end def test_compile_insn_toregexp - assert_compile_once('/#{true}/ =~ "true"', result_inspect: '0', insns: %i[toregexp]) + assert_compile_twice('/#{true}/ =~ "true"', result_inspect: '0', insns: %i[toregexp]) end def test_compile_insn_newarray - assert_compile_once("#{<<~"begin;"}\n#{<<~"end;"}", result_inspect: '[1, 2, 3]', insns: %i[newarray]) + assert_compile_twice("#{<<~"begin;"}\n#{<<~"end;"}", result_inspect: '[1, 2, 3]', insns: %i[newarray]) begin; a, b, c = 1, 2, 3 [a, b, c] @@ -257,39 +262,39 @@ class TestMJIT < Test::Unit::TestCase end def test_compile_insn_newarraykwsplat - assert_compile_once('[**{ x: 1 }]', result_inspect: '[{:x=>1}]', insns: %i[newarraykwsplat]) + assert_compile_twice('[**{ x: 1 }]', result_inspect: '[{:x=>1}]', insns: %i[newarraykwsplat]) end def test_compile_insn_intern_duparray - assert_compile_once('[:"#{0}"] + [1,2,3]', result_inspect: '[:"0", 1, 2, 3]', insns: %i[intern duparray]) + assert_compile_twice('[:"#{0}"] + [1,2,3]', result_inspect: '[:"0", 1, 2, 3]', insns: %i[intern duparray]) end def test_compile_insn_expandarray - assert_compile_once('y = [ true, false, nil ]; x, = y; x', result_inspect: 'true', insns: %i[expandarray]) + assert_compile_twice('y = [ true, false, nil ]; x, = y; x', result_inspect: 'true', insns: %i[expandarray]) end def test_compile_insn_concatarray - assert_compile_once('["t", "r", *x = "u", "e"].join', result_inspect: '"true"', insns: %i[concatarray]) + assert_compile_twice('["t", "r", *x = "u", "e"].join', result_inspect: '"true"', insns: %i[concatarray]) end def test_compile_insn_splatarray - assert_compile_once('[*(1..2)]', result_inspect: '[1, 2]', insns: %i[splatarray]) + assert_compile_twice('[*(1..2)]', result_inspect: '[1, 2]', insns: %i[splatarray]) end def test_compile_insn_newhash - assert_compile_once('a = 1; { a: a }', result_inspect: '{:a=>1}', insns: %i[newhash]) + assert_compile_twice('a = 1; { a: a }', result_inspect: '{:a=>1}', insns: %i[newhash]) end def test_compile_insn_duphash - assert_compile_once('{ a: 1 }', result_inspect: '{:a=>1}', insns: %i[duphash]) + assert_compile_twice('{ a: 1 }', result_inspect: '{:a=>1}', insns: %i[duphash]) end def test_compile_insn_newrange - assert_compile_once('a = 1; 0..a', result_inspect: '0..1', insns: %i[newrange]) + assert_compile_twice('a = 1; 0..a', result_inspect: '0..1', insns: %i[newrange]) end def test_compile_insn_pop - assert_compile_once("#{<<~"begin;"}\n#{<<~"end;"}", result_inspect: '1', insns: %i[pop]) + assert_compile_twice("#{<<~"begin;"}\n#{<<~"end;"}", result_inspect: '1', insns: %i[pop]) begin; a = false b = 1 @@ -298,7 +303,7 @@ class TestMJIT < Test::Unit::TestCase end def test_compile_insn_dup - assert_compile_once("#{<<~"begin;"}\n#{<<~"end;"}", result_inspect: '3', insns: %i[dup]) + assert_compile_twice("#{<<~"begin;"}\n#{<<~"end;"}", result_inspect: '3', insns: %i[dup]) begin; a = 1 a&.+(2) @@ -306,7 +311,7 @@ class TestMJIT < Test::Unit::TestCase end def test_compile_insn_dupn - assert_compile_once("#{<<~"begin;"}\n#{<<~"end;"}", result_inspect: 'true', insns: %i[dupn]) + assert_compile_twice("#{<<~"begin;"}\n#{<<~"end;"}", result_inspect: 'true', insns: %i[dupn]) begin; klass = Class.new klass::X ||= true @@ -314,7 +319,7 @@ class TestMJIT < Test::Unit::TestCase end def test_compile_insn_swap_topn - assert_compile_once('{}["true"] = true', result_inspect: 'true', insns: %i[swap topn]) + assert_compile_twice('{}["true"] = true', result_inspect: 'true', insns: %i[swap topn]) end def test_compile_insn_reput @@ -322,11 +327,11 @@ class TestMJIT < Test::Unit::TestCase end def test_compile_insn_setn - assert_compile_once('[nil][0] = 1', result_inspect: '1', insns: %i[setn]) + assert_compile_twice('[nil][0] = 1', result_inspect: '1', insns: %i[setn]) end def test_compile_insn_adjuststack - assert_compile_once("#{<<~"begin;"}\n#{<<~"end;"}", result_inspect: 'true', insns: %i[adjuststack]) + assert_compile_twice("#{<<~"begin;"}\n#{<<~"end;"}", result_inspect: 'true', insns: %i[adjuststack]) begin; x = [true] x[0] ||= nil @@ -335,16 +340,17 @@ class TestMJIT < Test::Unit::TestCase end def test_compile_insn_defined - assert_compile_once('defined?(a)', result_inspect: 'nil', insns: %i[defined]) + assert_compile_twice('defined?(a)', result_inspect: 'nil', insns: %i[defined]) end def test_compile_insn_checkkeyword - assert_eval_with_jit("#{<<~"begin;"}\n#{<<~"end;"}", stdout: 'true', success_count: 1, insns: %i[checkkeyword]) + assert_eval_with_jit("#{<<~"begin;"}\n#{<<~"end;"}", stdout: 'truetrue', success_count: 1, insns: %i[checkkeyword]) begin; def test(x: rand) x end print test(x: true) + print test(x: true) end; end @@ -357,35 +363,36 @@ class TestMJIT < Test::Unit::TestCase end def test_compile_insn_send - assert_eval_with_jit("#{<<~"begin;"}\n#{<<~"end;"}", stdout: '1', success_count: 3, insns: %i[send]) + assert_eval_with_jit("#{<<~"begin;"}\n#{<<~"end;"}", stdout: '11', success_count: 4, insns: %i[send]) begin; print proc { yield_self { 1 } }.call + print proc { yield_self { 1 } }.call end; end def test_compile_insn_opt_str_freeze - assert_compile_once("#{<<~"begin;"}\n#{<<~"end;"}", result_inspect: '"foo"', insns: %i[opt_str_freeze]) + assert_compile_twice("#{<<~"begin;"}\n#{<<~"end;"}", result_inspect: '"foo"', insns: %i[opt_str_freeze]) begin; 'foo'.freeze end; end def test_compile_insn_opt_nil_p - assert_compile_once("#{<<~"begin;"}\n#{<<~"end;"}", result_inspect: 'false', insns: %i[opt_nil_p]) + assert_compile_twice("#{<<~"begin;"}\n#{<<~"end;"}", result_inspect: 'false', insns: %i[opt_nil_p]) begin; nil.nil?.nil? end; end def test_compile_insn_opt_str_uminus - assert_compile_once("#{<<~"begin;"}\n#{<<~"end;"}", result_inspect: '"bar"', insns: %i[opt_str_uminus]) + assert_compile_twice("#{<<~"begin;"}\n#{<<~"end;"}", result_inspect: '"bar"', insns: %i[opt_str_uminus]) begin; -'bar' end; end def test_compile_insn_opt_newarray_max - assert_compile_once("#{<<~"begin;"}\n#{<<~"end;"}", result_inspect: '2', insns: %i[opt_newarray_max]) + assert_compile_twice("#{<<~"begin;"}\n#{<<~"end;"}", result_inspect: '2', insns: %i[opt_newarray_max]) begin; a = 1 b = 2 @@ -394,7 +401,7 @@ class TestMJIT < Test::Unit::TestCase end def test_compile_insn_opt_newarray_min - assert_compile_once("#{<<~"begin;"}\n#{<<~"end;"}", result_inspect: '1', insns: %i[opt_newarray_min]) + assert_compile_twice("#{<<~"begin;"}\n#{<<~"end;"}", result_inspect: '1', insns: %i[opt_newarray_min]) begin; a = 1 b = 2 @@ -403,11 +410,11 @@ class TestMJIT < Test::Unit::TestCase end def test_compile_insn_opt_send_without_block - assert_compile_once('print', result_inspect: 'nil', insns: %i[opt_send_without_block]) + assert_compile_twice('print', result_inspect: 'nil', insns: %i[opt_send_without_block]) end def test_compile_insn_invokesuper - assert_eval_with_jit("#{<<~"begin;"}\n#{<<~"end;"}", stdout: '3', success_count: 4, insns: %i[invokesuper]) + assert_eval_with_jit("#{<<~"begin;"}\n#{<<~"end;"}", stdout: '33', success_count: 4, insns: %i[invokesuper]) begin; mod = Module.new { def test @@ -421,21 +428,23 @@ class TestMJIT < Test::Unit::TestCase end } print klass.new.test + print klass.new.test end; end def test_compile_insn_invokeblock_leave - assert_eval_with_jit("#{<<~"begin;"}\n#{<<~"end;"}", stdout: '2', success_count: 2, insns: %i[invokeblock leave]) + assert_eval_with_jit("#{<<~"begin;"}\n#{<<~"end;"}", stdout: '22', success_count: 2, insns: %i[invokeblock leave]) begin; def foo yield end print foo { 2 } + print foo { 2 } end; end def test_compile_insn_throw - assert_eval_with_jit("#{<<~"begin;"}\n#{<<~"end;"}", stdout: '4', success_count: 2, insns: %i[throw]) + assert_eval_with_jit("#{<<~"begin;"}\n#{<<~"end;"}", stdout: '44', success_count: 2, insns: %i[throw]) begin; def test proc do @@ -448,11 +457,12 @@ class TestMJIT < Test::Unit::TestCase end.call end print test + print test end; end def test_compile_insn_jump_branchif - assert_compile_once("#{<<~"begin;"}\n#{<<~'end;'}", result_inspect: 'nil', insns: %i[jump branchif]) + assert_compile_twice("#{<<~"begin;"}\n#{<<~'end;'}", result_inspect: 'nil', insns: %i[jump branchif]) begin; a = false 1 + 1 while a @@ -460,7 +470,7 @@ class TestMJIT < Test::Unit::TestCase end def test_compile_insn_branchunless - assert_compile_once("#{<<~"begin;"}\n#{<<~'end;'}", result_inspect: '1', insns: %i[branchunless]) + assert_compile_twice("#{<<~"begin;"}\n#{<<~'end;'}", result_inspect: '1', insns: %i[branchunless]) begin; a = true if a @@ -472,7 +482,7 @@ class TestMJIT < Test::Unit::TestCase end def test_compile_insn_branchnil - assert_compile_once("#{<<~"begin;"}\n#{<<~'end;'}", result_inspect: '3', insns: %i[branchnil]) + assert_compile_twice("#{<<~"begin;"}\n#{<<~'end;'}", result_inspect: '3', insns: %i[branchnil]) begin; a = 2 a&.+(1) @@ -480,7 +490,7 @@ class TestMJIT < Test::Unit::TestCase end def test_compile_insn_objtostring - assert_compile_once("#{<<~"begin;"}\n#{<<~'end;'}", result_inspect: '"42"', insns: %i[objtostring]) + assert_compile_twice("#{<<~"begin;"}\n#{<<~'end;'}", result_inspect: '"42"', insns: %i[objtostring]) begin; a = '2' "4#{a}" @@ -488,15 +498,15 @@ class TestMJIT < Test::Unit::TestCase end def test_compile_insn_getconstant_path - assert_compile_once('Struct', result_inspect: 'Struct', insns: %i[opt_getconstant_path]) + assert_compile_twice('Struct', result_inspect: 'Struct', insns: %i[opt_getconstant_path]) end def test_compile_insn_once - assert_compile_once('/#{true}/o =~ "true" && $~.to_a', result_inspect: '["true"]', insns: %i[once]) + assert_compile_twice('/#{true}/o =~ "true" && $~.to_a', result_inspect: '["true"]', insns: %i[once]) end def test_compile_insn_checkmatch_opt_case_dispatch - assert_compile_once("#{<<~"begin;"}\n#{<<~"end;"}", result_inspect: '"world"', insns: %i[opt_case_dispatch]) + assert_compile_twice("#{<<~"begin;"}\n#{<<~"end;"}", result_inspect: '"world"', insns: %i[opt_case_dispatch]) begin; case 'hello' when 'hello' @@ -506,34 +516,34 @@ class TestMJIT < Test::Unit::TestCase end def test_compile_insn_opt_calc - assert_compile_once('4 + 2 - ((2 * 3 / 2) % 2)', result_inspect: '5', insns: %i[opt_plus opt_minus opt_mult opt_div opt_mod]) - assert_compile_once('4.0 + 2.0 - ((2.0 * 3.0 / 2.0) % 2.0)', result_inspect: '5.0', insns: %i[opt_plus opt_minus opt_mult opt_div opt_mod]) - assert_compile_once('4 + 2', result_inspect: '6') + assert_compile_twice('4 + 2 - ((2 * 3 / 2) % 2)', result_inspect: '5', insns: %i[opt_plus opt_minus opt_mult opt_div opt_mod]) + assert_compile_twice('4.0 + 2.0 - ((2.0 * 3.0 / 2.0) % 2.0)', result_inspect: '5.0', insns: %i[opt_plus opt_minus opt_mult opt_div opt_mod]) + assert_compile_twice('4 + 2', result_inspect: '6') end def test_compile_insn_opt_cmp - assert_compile_once('(1 == 1) && (1 != 2)', result_inspect: 'true', insns: %i[opt_eq opt_neq]) + assert_compile_twice('(1 == 1) && (1 != 2)', result_inspect: 'true', insns: %i[opt_eq opt_neq]) end def test_compile_insn_opt_rel - assert_compile_once('1 < 2 && 1 <= 1 && 2 > 1 && 1 >= 1', result_inspect: 'true', insns: %i[opt_lt opt_le opt_gt opt_ge]) + assert_compile_twice('1 < 2 && 1 <= 1 && 2 > 1 && 1 >= 1', result_inspect: 'true', insns: %i[opt_lt opt_le opt_gt opt_ge]) end def test_compile_insn_opt_ltlt - assert_compile_once('[1] << 2', result_inspect: '[1, 2]', insns: %i[opt_ltlt]) + assert_compile_twice('[1] << 2', result_inspect: '[1, 2]', insns: %i[opt_ltlt]) end def test_compile_insn_opt_and - assert_compile_once('1 & 3', result_inspect: '1', insns: %i[opt_and]) + assert_compile_twice('1 & 3', result_inspect: '1', insns: %i[opt_and]) end def test_compile_insn_opt_or - assert_compile_once('1 | 3', result_inspect: '3', insns: %i[opt_or]) + assert_compile_twice('1 | 3', result_inspect: '3', insns: %i[opt_or]) end def test_compile_insn_opt_aref # optimized call (optimized JIT) -> send call - assert_eval_with_jit("#{<<~"begin;"}\n#{<<~"end;"}", stdout: '21', success_count: 2, call_threshold: 1, insns: %i[opt_aref]) + assert_eval_with_jit("#{<<~"begin;"}\n#{<<~"end;"}", stdout: '221', success_count: 1, call_threshold: 2, insns: %i[opt_aref]) begin; obj = Object.new def obj.[](h) @@ -542,11 +552,12 @@ class TestMJIT < Test::Unit::TestCase block = proc { |h| h[1] } print block.call({ 1 => 2 }) + print block.call({ 1 => 2 }) print block.call(obj) end; # send call -> optimized call (send JIT) -> optimized call - assert_eval_with_jit("#{<<~"begin;"}\n#{<<~"end;"}", stdout: '122', success_count: 2, call_threshold: 2) + assert_eval_with_jit("#{<<~"begin;"}\n#{<<~"end;"}", stdout: '122', success_count: 3, call_threshold: 2) begin; obj = Object.new def obj.[](h) @@ -561,11 +572,11 @@ class TestMJIT < Test::Unit::TestCase end def test_compile_insn_opt_aref_with - assert_compile_once("{ '1' => 2 }['1']", result_inspect: '2', insns: %i[opt_aref_with]) + assert_compile_twice("{ '1' => 2 }['1']", result_inspect: '2', insns: %i[opt_aref_with]) end def test_compile_insn_opt_aset - assert_compile_once("#{<<~"begin;"}\n#{<<~"end;"}", result_inspect: '5', insns: %i[opt_aset opt_aset_with]) + assert_compile_twice("#{<<~"begin;"}\n#{<<~"end;"}", result_inspect: '5', insns: %i[opt_aset opt_aset_with]) begin; hash = { '1' => 2 } (hash['2'] = 2) + (hash[1.to_s] = 3) @@ -573,7 +584,7 @@ class TestMJIT < Test::Unit::TestCase end def test_compile_insn_opt_length_size - assert_compile_once("#{<<~"begin;"}\n#{<<~"end;"}", result_inspect: '4', insns: %i[opt_length opt_size]) + assert_compile_twice("#{<<~"begin;"}\n#{<<~"end;"}", result_inspect: '4', insns: %i[opt_length opt_size]) begin; array = [1, 2] array.length + array.size @@ -581,20 +592,20 @@ class TestMJIT < Test::Unit::TestCase end def test_compile_insn_opt_empty_p - assert_compile_once('[].empty?', result_inspect: 'true', insns: %i[opt_empty_p]) + assert_compile_twice('[].empty?', result_inspect: 'true', insns: %i[opt_empty_p]) end def test_compile_insn_opt_succ - assert_compile_once('1.succ', result_inspect: '2', insns: %i[opt_succ]) + assert_compile_twice('1.succ', result_inspect: '2', insns: %i[opt_succ]) end def test_compile_insn_opt_not - assert_compile_once('!!true', result_inspect: 'true', insns: %i[opt_not]) + assert_compile_twice('!!true', result_inspect: 'true', insns: %i[opt_not]) end def test_compile_insn_opt_regexpmatch2 - assert_compile_once("/true/ =~ 'true'", result_inspect: '0', insns: %i[opt_regexpmatch2]) - assert_compile_once("'true' =~ /true/", result_inspect: '0', insns: %i[opt_regexpmatch2]) + assert_compile_twice("/true/ =~ 'true'", result_inspect: '0', insns: %i[opt_regexpmatch2]) + assert_compile_twice("'true' =~ /true/", result_inspect: '0', insns: %i[opt_regexpmatch2]) end def test_compile_insn_invokebuiltin @@ -603,7 +614,7 @@ class TestMJIT < Test::Unit::TestCase EOS insns = collect_insns(iseq) mark_tested_insn(:invokebuiltin, used_insns: insns) - assert_eval_with_jit('print [].sample(1)', stdout: '[]', success_count: 1) + assert_eval_with_jit('print [].sample(1); print [].sample(1)', stdout: '[][]', success_count: 1) end def test_compile_insn_opt_invokebuiltin_delegate_leave @@ -612,11 +623,11 @@ class TestMJIT < Test::Unit::TestCase EOS insns = collect_insns(iseq) mark_tested_insn(:opt_invokebuiltin_delegate_leave, used_insns: insns) - assert_eval_with_jit('print "\x00".unpack("c")', stdout: '[0]', success_count: 1) + assert_eval_with_jit('print "\x00".unpack("c");print "\x00".unpack("c")', stdout: '[0][0]', success_count: 1) end def test_compile_insn_checkmatch - assert_compile_once("#{<<~"begin;"}\n#{<<~"end;"}", result_inspect: '"world"', insns: %i[checkmatch]) + assert_compile_twice("#{<<~"begin;"}\n#{<<~"end;"}", result_inspect: '"world"', insns: %i[checkmatch]) begin; ary = %w(hello good-bye) case 'hello' @@ -627,23 +638,25 @@ class TestMJIT < Test::Unit::TestCase end def test_compile_opt_pc - assert_eval_with_jit("#{<<~"begin;"}\n#{<<~"end;"}", stdout: 'hello', success_count: 1) + assert_eval_with_jit("#{<<~"begin;"}\n#{<<~"end;"}", stdout: 'hellohello', success_count: 1) begin; def test(arg = 'hello') print arg end test + test end; end def test_mjit_output - out, err = eval_with_jit('5.times { puts "MJIT" }', verbose: 1, call_threshold: 5) - assert_equal("MJIT\n" * 5, out) - assert_match(/^#{JIT_SUCCESS_PREFIX}: block in <main>@-e:1 -> .+_ruby_mjit_p\d+u\d+\.c$/, err) + out, err = eval_with_jit('4.times { puts "MJIT" }', verbose: 1, call_threshold: 2) + assert_equal("MJIT\n" * 4, out) + assert_match(/^#{JIT_SUCCESS_PREFIX}: block in <main>@-e:1$/, err) assert_match(/^Successful MJIT finish$/, err) end def test_nothing_to_unload_with_jit_wait + omit 'unload_units is removed for now' assert_eval_with_jit("#{<<~"begin;"}\n#{<<~"end;"}", stdout: 'hello', success_count: 11, max_cache: 10, ignorable_patterns: MAX_CACHE_PATTERNS) begin; def a1() a2() end @@ -662,6 +675,7 @@ class TestMJIT < Test::Unit::TestCase end def test_unload_units_on_fiber + omit 'unload_units is removed for now' assert_eval_with_jit("#{<<~"begin;"}\n#{<<~"end;"}", stdout: 'hello', success_count: 12, max_cache: 10, ignorable_patterns: MAX_CACHE_PATTERNS) begin; def a1() a2(false); a2(true) end @@ -684,9 +698,10 @@ class TestMJIT < Test::Unit::TestCase end def test_unload_units_and_compaction + omit 'unload_units is removed for now' Dir.mktmpdir("jit_test_unload_units_") do |dir| # MIN_CACHE_SIZE is 10 - out, err = eval_with_jit({"TMPDIR"=>dir}, "#{<<~"begin;"}\n#{<<~'end;'}", verbose: 1, call_threshold: 1, max_cache: 10) + out, err = eval_with_jit({"TMPDIR"=>dir}, "#{<<~"begin;"}\n#{<<~'end;'}", verbose: 1, call_threshold: 2, max_cache: 10) begin; i = 0 while i < 11 @@ -741,12 +756,13 @@ class TestMJIT < Test::Unit::TestCase def arr [nil, [:type => :development]] end + arr p arr end; end def test_local_stack_on_exception - assert_eval_with_jit("#{<<~"begin;"}\n#{<<~"end;"}", stdout: '3', success_count: 2) + assert_eval_with_jit("#{<<~"begin;"}\n#{<<~"end;"}", stdout: '33', success_count: 2) begin; def b raise @@ -761,11 +777,12 @@ class TestMJIT < Test::Unit::TestCase end print a + print a end; end def test_local_stack_with_sp_motion_by_blockargs - assert_eval_with_jit("#{<<~"begin;"}\n#{<<~"end;"}", stdout: '1', success_count: 2) + assert_eval_with_jit("#{<<~"begin;"}\n#{<<~"end;"}", stdout: '11', success_count: 2) begin; def b(base) 1 @@ -782,11 +799,12 @@ class TestMJIT < Test::Unit::TestCase end print a + print a end; end def test_catching_deep_exception - assert_eval_with_jit("#{<<~"begin;"}\n#{<<~"end;"}", stdout: '1', success_count: 4) + assert_eval_with_jit("#{<<~"begin;"}\n#{<<~"end;"}", stdout: '11', success_count: 4) begin; def catch_true(paths, prefixes) # catch_except_p: true prefixes.each do |prefix| # catch_except_p: true @@ -801,6 +819,7 @@ class TestMJIT < Test::Unit::TestCase end print wrapper(['1'], ['2']) + print wrapper(['1'], ['2']) end; end @@ -819,7 +838,7 @@ class TestMJIT < Test::Unit::TestCase end def test_inlined_c_method - assert_eval_with_jit("#{<<~"begin;"}\n#{<<~"end;"}", stdout: "aaa", success_count: 2, recompile_count: 1, call_threshold: 2) + assert_eval_with_jit("#{<<~"begin;"}\n#{<<~"end;"}", stdout: "aaa", success_count: 1, recompile_count: 1, call_threshold: 2) begin; def test(obj, recursive: nil) if recursive @@ -856,7 +875,7 @@ class TestMJIT < Test::Unit::TestCase end def test_inlined_undefined_ivar - assert_eval_with_jit("#{<<~"begin;"}\n#{<<~"end;"}", stdout: "bbb", success_count: 5, call_threshold: 2) + assert_eval_with_jit("#{<<~"begin;"}\n#{<<~"end;"}", stdout: "bbb", success_count: 4, call_threshold: 2) begin; class Foo def initialize @@ -877,7 +896,7 @@ class TestMJIT < Test::Unit::TestCase end def test_inlined_setivar_frozen - assert_eval_with_jit("#{<<~"begin;"}\n#{<<~"end;"}", stdout: "FrozenError\n", success_count: 2, call_threshold: 3) + assert_eval_with_jit("#{<<~"begin;"}\n#{<<~"end;"}", stdout: "FrozenError\n", success_count: 1, call_threshold: 2) begin; class A def a @@ -911,7 +930,7 @@ class TestMJIT < Test::Unit::TestCase end def test_attr_reader - assert_eval_with_jit("#{<<~"begin;"}\n#{<<~"end;"}", stdout: "4nil\nnil\n6", success_count: 2, call_threshold: 2) + assert_eval_with_jit("#{<<~"begin;"}\n#{<<~"end;"}", stdout: "4nil\nnil\n6", success_count: 3, call_threshold: 2) begin; class A attr_reader :a, :b @@ -942,7 +961,7 @@ class TestMJIT < Test::Unit::TestCase print(2 * a.test) end; - assert_eval_with_jit("#{<<~"begin;"}\n#{<<~"end;"}", stdout: "true", success_count: 1, call_threshold: 2) + assert_eval_with_jit("#{<<~"begin;"}\n#{<<~"end;"}", stdout: "true", success_count: 3, call_threshold: 2) begin; class Hoge attr_reader :foo @@ -1004,12 +1023,13 @@ class TestMJIT < Test::Unit::TestCase end def test_jump_to_precompiled_branch - assert_eval_with_jit("#{<<~'begin;'}\n#{<<~'end;'}", stdout: ".0", success_count: 1, call_threshold: 1) + assert_eval_with_jit("#{<<~'begin;'}\n#{<<~'end;'}", stdout: ".0.0", success_count: 1, call_threshold: 2) begin; def test(foo) ".#{foo unless foo == 1}" if true end print test(0) + print test(0) end; end @@ -1038,7 +1058,7 @@ class TestMJIT < Test::Unit::TestCase omit '.bundle.dSYM directory is left but removing it is not supported for now' end Dir.mktmpdir("jit_test_clean_objects_on_exec_") do |dir| - eval_with_jit({"TMPDIR"=>dir}, "#{<<~"begin;"}\n#{<<~"end;"}", call_threshold: 1) + eval_with_jit({"TMPDIR"=>dir}, "#{<<~"begin;"}\n#{<<~"end;"}", call_threshold: 2) begin; def a; end; a exec "true" @@ -1070,7 +1090,7 @@ class TestMJIT < Test::Unit::TestCase end def test_frame_omitted_inlining - assert_eval_with_jit("#{<<~"begin;"}\n#{<<~"end;"}", stdout: "true\ntrue\ntrue\n", success_count: 1, call_threshold: 2) + assert_eval_with_jit("#{<<~"begin;"}\n#{<<~"end;"}", stdout: "true\ntrue\ntrue\ntrue\n", success_count: 2, call_threshold: 2) begin; class Integer remove_method :zero? @@ -1079,20 +1099,20 @@ class TestMJIT < Test::Unit::TestCase end end - 3.times do + 4.times do p 0.zero? end end; end def test_block_handler_with_possible_frame_omitted_inlining - assert_eval_with_jit("#{<<~"begin;"}\n#{<<~"end;"}", stdout: "70.0\n70.0\n70.0\n", success_count: 2, call_threshold: 2) + assert_eval_with_jit("#{<<~"begin;"}\n#{<<~"end;"}", stdout: "70.0\n70.0\n70.0\n70.0\n", success_count: 2, call_threshold: 2) begin; def multiply(a, b) a *= b end - 3.times do + 4.times do p multiply(7.0, 10.0) end end; @@ -1131,7 +1151,7 @@ class TestMJIT < Test::Unit::TestCase end def test_mjit_pause_wait - assert_eval_with_jit("#{<<~"begin;"}\n#{<<~"end;"}", stdout: '', success_count: 0, call_threshold: 1) + assert_eval_with_jit("#{<<~"begin;"}\n#{<<~"end;"}", stdout: '', success_count: 0, call_threshold: 2) begin; RubyVM::MJIT.pause proc {}.call @@ -1139,7 +1159,7 @@ class TestMJIT < Test::Unit::TestCase end def test_not_cancel_by_tracepoint_class - assert_eval_with_jit("#{<<~"begin;"}\n#{<<~"end;"}", success_count: 1, call_threshold: 2) + assert_eval_with_jit("#{<<~"begin;"}\n#{<<~"end;"}", success_count: 3, call_threshold: 2) begin; TracePoint.new(:class) {}.enable 2.times {} @@ -1155,7 +1175,7 @@ class TestMJIT < Test::Unit::TestCase end def test_caller_locations_without_catch_table - out, _ = eval_with_jit("#{<<~"begin;"}\n#{<<~"end;"}", call_threshold: 1) + out, _ = eval_with_jit("#{<<~"begin;"}\n#{<<~"end;"}", call_threshold: 2) begin; def b # 2 caller_locations.first # 3 @@ -1173,64 +1193,31 @@ class TestMJIT < Test::Unit::TestCase assert_equal("-e:8:in `a'\n", lines[1]) end - def test_fork_with_mjit_worker_thread - Dir.mktmpdir("jit_test_fork_with_mjit_worker_thread_") do |dir| - # call_threshold: 2 to skip fork block - out, err = eval_with_jit({ "TMPDIR" => dir }, "#{<<~"begin;"}\n#{<<~"end;"}", call_threshold: 2, verbose: 1) - begin; - def before_fork; end - def after_fork; end - - before_fork; before_fork # the child should not delete this .o file - pid = Process.fork do # this child should not delete shared .pch file - sleep 2.0 # to prevent mixing outputs on Solaris - after_fork; after_fork # this child does not share JIT-ed after_fork with parent - end - after_fork; after_fork # this parent does not share JIT-ed after_fork with child - - Process.waitpid(pid) - end; - success_count = err.scan(/^#{JIT_SUCCESS_PREFIX}:/).size - debug_info = "stdout:\n```\n#{out}\n```\n\nstderr:\n```\n#{err}```\n" - assert_equal(3, success_count, debug_info) - - # assert no remove error - assert_equal("Successful MJIT finish\n" * 2, err.gsub(/^#{JIT_SUCCESS_PREFIX}:[^\n]+\n/, ''), debug_info) - - # ensure objects are deleted - if RUBY_PLATFORM.match?(/darwin/) - omit '.bundle.dSYM directory is left but removing it is not supported for now' - end - assert_send([Dir, :empty?, dir], debug_info) - end - end if defined?(fork) - def test_jit_failure - _, err = eval_with_jit("#{<<~"begin;"}\n#{<<~"end;"}", call_threshold: 1, verbose: 1) + _, err = eval_with_jit("#{<<~"begin;"}\n#{<<~"end;"}", call_threshold: 2, verbose: 1) begin; - 1.times do + 2.times do class A end end end; assert_match(/^MJIT warning: .+ unsupported instruction: defineclass/, err) - assert_match(/^JIT failure: block in <main>/, err) end private # The shortest way to test one proc - def assert_compile_once(script, result_inspect:, insns: [], uplevel: 1) + def assert_compile_twice(script, result_inspect:, insns: [], uplevel: 1) if script.match?(/\A\n.+\n\z/m) script = script.gsub(/^/, ' ') else script = " #{script} " end - assert_eval_with_jit("p proc {#{script}}.call", stdout: "#{result_inspect}\n", success_count: 1, insns: insns, uplevel: uplevel + 1) + assert_eval_with_jit("test = proc {#{script}}; p test.call; p test.call", stdout: "#{result_inspect}\n#{result_inspect}\n", success_count: 1, insns: insns, uplevel: uplevel + 1) end # Shorthand for normal test cases - def assert_eval_with_jit(script, stdout: nil, success_count:, recompile_count: nil, call_threshold: 1, max_cache: 1000, insns: [], uplevel: 1, ignorable_patterns: []) + def assert_eval_with_jit(script, stdout: nil, success_count:, recompile_count: nil, call_threshold: 2, max_cache: 1000, insns: [], uplevel: 1, ignorable_patterns: []) out, err = eval_with_jit(script, verbose: 1, call_threshold: call_threshold, max_cache: max_cache) success_actual = err.scan(/^#{JIT_SUCCESS_PREFIX}:/).size recompile_actual = err.scan(/^#{JIT_RECOMPILE_PREFIX}:/).size diff --git a/test/ruby/test_rubyvm_mjit.rb b/test/ruby/test_rubyvm_mjit.rb index 90ff2edc8e..73f43af7f9 100644 --- a/test/ruby/test_rubyvm_mjit.rb +++ b/test/ruby/test_rubyvm_mjit.rb @@ -14,10 +14,10 @@ class TestRubyVMMJIT < Test::Unit::TestCase end def test_pause - out, err = eval_with_jit(<<~'EOS', verbose: 1, call_threshold: 1, wait: false) + out, err = eval_with_jit(<<~'EOS', verbose: 1, call_threshold: 2, wait: false) i = 0 while i < 5 - eval("def mjit#{i}; end; mjit#{i}") + eval("def mjit#{i}; end; mjit#{i}; mjit#{i}") i += 1 end print RubyVM::MJIT.pause @@ -36,25 +36,22 @@ class TestRubyVMMJIT < Test::Unit::TestCase end def test_pause_waits_until_compaction - out, err = eval_with_jit(<<~'EOS', verbose: 1, call_threshold: 1, wait: false) - def a() end; a - def b() end; b + out, err = eval_with_jit(<<~'EOS', verbose: 1, call_threshold: 2, wait: false) + def a() end; a; a + def b() end; b; b RubyVM::MJIT.pause EOS assert_equal( 2, err.scan(/#{JITSupport::JIT_SUCCESS_PREFIX}/).size, "unexpected stdout:\n```\n#{out}```\n\nstderr:\n```\n#{err}```", ) - assert_equal( - 1, err.scan(/#{JITSupport::JIT_COMPACTION_PREFIX}/).size, - "unexpected stdout:\n```\n#{out}```\n\nstderr:\n```\n#{err}```", - ) unless RUBY_PLATFORM.match?(/mswin|mingw/) # compaction is not supported on Windows yet end def test_pause_after_waitall - out, err = eval_with_jit(<<~'EOS', verbose: 1, call_threshold: 1, wait: false) + out, err = eval_with_jit(<<~'EOS', verbose: 1, call_threshold: 2, wait: false) def test() = nil test + test Process.waitall print RubyVM::MJIT.pause EOS @@ -65,7 +62,7 @@ class TestRubyVMMJIT < Test::Unit::TestCase end def test_pause_does_not_hang_on_full_units - out, _ = eval_with_jit(<<~'EOS', verbose: 1, call_threshold: 1, max_cache: 10, wait: false) + out, _ = eval_with_jit(<<~'EOS', verbose: 1, call_threshold: 2, max_cache: 10, wait: false) i = 0 while i < 11 eval("def mjit#{i}; end; mjit#{i}") @@ -77,7 +74,7 @@ class TestRubyVMMJIT < Test::Unit::TestCase end def test_pause_wait_false - out, err = eval_with_jit(<<~'EOS', verbose: 1, call_threshold: 1, wait: false) + out, err = eval_with_jit(<<~'EOS', verbose: 1, call_threshold: 2, wait: false) i = 0 while i < 10 eval("def mjit#{i}; end; mjit#{i}") @@ -95,7 +92,7 @@ class TestRubyVMMJIT < Test::Unit::TestCase end def test_resume - out, err = eval_with_jit(<<~'EOS', verbose: 1, call_threshold: 1, wait: false) + out, err = eval_with_jit(<<~'EOS', verbose: 1, call_threshold: 2, wait: false) print RubyVM::MJIT.resume print RubyVM::MJIT.pause print RubyVM::MJIT.resume |