[mini] Introduce job control to the JIT. Limits by active and duplicate jobs.
authorRodrigo Kumpera <kumpera@gmail.com>
Thu, 8 Dec 2016 00:10:41 +0000 (16:10 -0800)
committerRodrigo Kumpera <kumpera@gmail.com>
Thu, 6 Apr 2017 23:09:00 +0000 (16:09 -0700)
This shown up as a problem under parallel roslyn.

We hit frequently the case of multiple threads compiling the same method in parallel
and wasting all but one.

Another issue that happens, but infrequently, is having more threads JITing than there
are cores in the machine. Such thrashing doesn't help.

Test setup. 4/8 macbook pro compiling corlib.

Baseline:
real 0m7.665s
user 0m20.078s
sys 0m2.653s
Methods JITted using mono JIT       : 22422
Total time spent JITting (sec)      : 18.5365

With this patch:
real 0m6.149s
user 0m18.504s
sys 0m1.487s
Methods JITted using mono JIT       : 16619
Total time spent JITting (sec)      : 4.9420

New counters
JIT compile waited others           : 7681
JIT compile 1+ jobs                 : 1
JIT compile overload wait           : 67
JIT compile spurious wakeups        : 14469

This results in a 20% wall clock reduction but only a 8% reduction on user.

We JIT 26% less methods, with very few duplications. Showing this drastically improves the situation.

JIT compilation time metrics are bogus due to .cctors and other sources of interference. So take it with a grain of salt.

Future work:

Based on the new counters, it's clear that the current wakeup design is suboptimal and we could further improve it by cutting on spurious wakeups.

mono/mini/mini-runtime.c
mono/mini/mini.h

index 011029de9fa57dc8a63966823bc9be92dc8f4fdf..908c9d6ffa73ef4c5a9a10ae53f5a2b02ecc50a3 100644 (file)
@@ -65,6 +65,7 @@
 #include <mono/utils/mono-threads.h>
 #include <mono/utils/mono-threads-coop.h>
 #include <mono/utils/checked-build.h>
+#include <mono/utils/mono-proclib.h>
 #include <mono/metadata/w32handle.h>
 #include <mono/metadata/threadpool.h>
 
@@ -1756,6 +1757,191 @@ no_gsharedvt_in_wrapper (void)
        g_assert_not_reached ();
 }
 
+/*
+Overall algorithm:
+
+Limit JITing to mono_cpu_count
+       This ensures there's always room for application progress and not just JITing.
+
+When a JIT request is made, we check if there's an outstanding one for that method and, if it exits, put the thread to sleep.
+       If the current thread is already JITing another method, don't wait as it might cause a deadlock.
+       Dependency management in this case is too complex to justify implementing it.
+
+If there are no outstanding requests, the current thread is doing nothing and there are already mono_cpu_count threads JITing, go to sleep.
+
+TODO:
+       Get rid of cctor invocatio from within the JIT, it increases JIT duration and complicates things A LOT.
+       Verify that we don't have too many spurious wakeups.
+       Experiment with limiting to values around mono_cpu_count +/- 1 as this would enable progress during warmup.
+*/
+typedef struct {
+       MonoMethod *method;
+       MonoDomain *domain;
+       int count;
+} JitCompilationEntry;
+
+typedef struct {
+       GPtrArray *in_flight_methods; //JitCompilationEntry*
+       int active_threads;
+       int threads_waiting;
+       MonoCoopMutex lock;
+       MonoCoopCond cond;
+} JitCompilationData;
+
+static JitCompilationData compilation_data;
+static int jit_methods_waited, jit_methods_multiple, jit_methods_overload, jit_spurious_wakeups;
+
+static void
+mini_jit_init_job_control (void)
+{
+       mono_coop_mutex_init (&compilation_data.lock);
+       mono_coop_cond_init (&compilation_data.cond);
+       compilation_data.in_flight_methods = g_ptr_array_new ();
+}
+
+static void
+lock_compilation_data (void)
+{
+       mono_coop_mutex_lock (&compilation_data.lock);
+       fflush (stdout);
+}
+
+static void
+unlock_compilation_data (void)
+{
+       fflush (stdout);
+       mono_coop_mutex_unlock (&compilation_data.lock);
+}
+
+static JitCompilationEntry*
+find_method (MonoMethod *method, MonoDomain *domain)
+{
+       int i;
+       for (i = 0; i < compilation_data.in_flight_methods->len; ++i){
+               JitCompilationEntry *e = compilation_data.in_flight_methods->pdata [i];
+               if (e->method == method && e->domain == domain)
+                       return e;
+       }
+
+       return NULL;
+}
+
+static void
+add_current_thread (MonoJitTlsData *jit_tls)
+{
+       if (jit_tls->active_jit_methods == 0)
+               ++compilation_data.active_threads;
+       ++jit_tls->active_jit_methods;
+}
+
+//Returns true if this thread should wait
+static gboolean
+should_wait_for_available_cpu_capacity (void)
+{
+       MonoJitTlsData *jit_tls = mono_native_tls_get_value (mono_jit_tls_id);
+
+       //We can't suspend threads that are already JIT'ing something or we risk deadlocking
+       if (jit_tls->active_jit_methods > 0)
+               return FALSE;
+
+       //If there are as many active threads as cores, JITing more will cause thrashing
+       if (compilation_data.active_threads >= mono_cpu_count ()) {
+               ++jit_methods_overload;
+               return TRUE;
+       }
+       return FALSE;
+}
+
+/*
+ * Returns true if this method waited successfully for another thread to JIT it
+ */
+static gboolean
+wait_or_register_method_to_compile (MonoMethod *method, MonoDomain *domain)
+{
+       MonoJitTlsData *jit_tls = mono_native_tls_get_value (mono_jit_tls_id);
+       JitCompilationEntry *entry;
+
+
+       static gboolean inited;
+       if (!inited) {
+               mono_counters_register ("JIT compile waited others", MONO_COUNTER_INT|MONO_COUNTER_JIT, &jit_methods_waited);
+               mono_counters_register ("JIT compile 1+ jobs", MONO_COUNTER_INT|MONO_COUNTER_JIT, &jit_methods_multiple);
+               mono_counters_register ("JIT compile overload wait", MONO_COUNTER_INT|MONO_COUNTER_JIT, &jit_methods_overload);
+               mono_counters_register ("JIT compile spurious wakeups", MONO_COUNTER_INT|MONO_COUNTER_JIT, &jit_spurious_wakeups);
+               inited = TRUE;
+       }
+
+       lock_compilation_data ();
+
+       int waits = 0;
+       while (should_wait_for_available_cpu_capacity ()) {
+               fflush (stdout);
+               ++compilation_data.threads_waiting;
+               mono_coop_cond_wait (&compilation_data.cond, &compilation_data.lock);
+               --compilation_data.threads_waiting;
+               if (waits)
+                       ++jit_spurious_wakeups;
+               ++waits;
+       }
+
+       if (!(entry = find_method (method, domain))) {
+               entry = g_new (JitCompilationEntry, 1);
+               entry->method = method;
+               entry->domain = domain;
+               entry->count = 1;
+               g_ptr_array_add (compilation_data.in_flight_methods, entry);
+               add_current_thread (jit_tls);
+               unlock_compilation_data ();
+               return FALSE;
+       } else if (jit_tls->active_jit_methods > 0) {
+               //We can't suspend the current thread if it's already JITing a method.
+               //Dependency management is too compilated and we want to get rid of this anyways.
+               ++entry->count;
+               ++jit_methods_multiple;
+               unlock_compilation_data ();
+               return FALSE;
+       } else {
+               ++jit_methods_waited;
+               while (TRUE) {
+                       fflush (stdout);
+                       ++compilation_data.threads_waiting;
+                       mono_coop_cond_wait (&compilation_data.cond, &compilation_data.lock);
+                       --compilation_data.threads_waiting;
+
+                       if (!find_method (method, domain)) {
+                               unlock_compilation_data ();
+                               return TRUE;
+                       } else {
+                               ++jit_spurious_wakeups;
+                       }
+               }
+       }
+}
+
+static void
+unregister_method_for_compile (MonoMethod *method, MonoDomain *target_domain)
+{
+       MonoJitTlsData *jit_tls = mono_native_tls_get_value (mono_jit_tls_id);
+
+       lock_compilation_data ();
+
+       --jit_tls->active_jit_methods;
+       if (jit_tls->active_jit_methods == 0)
+               --compilation_data.active_threads;
+
+       JitCompilationEntry *entry = find_method (method, target_domain);
+       g_assert (entry); // It would be weird to fail
+       if (--entry->count == 0) {
+               g_ptr_array_remove (compilation_data.in_flight_methods, entry);
+               g_free (entry);
+       }
+
+       if (compilation_data.threads_waiting)
+               mono_coop_cond_broadcast (&compilation_data.cond);
+       unlock_compilation_data ();
+}
+
+
 static gpointer
 mono_jit_compile_method_with_opt (MonoMethod *method, guint32 opt, gboolean jit_only, MonoError *error)
 {
@@ -1818,6 +2004,7 @@ mono_jit_compile_method_with_opt (MonoMethod *method, guint32 opt, gboolean jit_
                }
        }
 
+lookup_start:
        info = lookup_method (target_domain, method);
        if (info) {
                /* We can't use a domain specific method in another domain */
@@ -1885,8 +2072,12 @@ mono_jit_compile_method_with_opt (MonoMethod *method, guint32 opt, gboolean jit_
                }
        }
 
-       if (!code)
+       if (!code) {
+               if (wait_or_register_method_to_compile (method, target_domain))
+                       goto lookup_start;
                code = mono_jit_compile_method_inner (method, target_domain, opt, error);
+               unregister_method_for_compile (method, target_domain);
+       }
        if (!mono_error_ok (error))
                return NULL;
 
@@ -3553,6 +3744,8 @@ mini_init (const char *filename, const char *runtime_version)
 
        mini_jit_init ();
 
+       mini_jit_init_job_control ();
+
        /* Happens when using the embedding interface */
        if (!default_opt_set)
                default_opt = mono_parse_default_optimizations (NULL);
index 565199d518de2b0f6a423e0345e5d18948891338..191386a06bb5032ddeeb6e57b40e2fc81763080a 100644 (file)
@@ -1202,6 +1202,12 @@ typedef struct {
         * the catch block that caught the ThreadAbortException).
         */
        gpointer abort_exc_stack_threshold;
+
+
+       /*
+        * List of methods being JIT'd in the current thread.
+        */
+       int active_jit_methods;
 } MonoJitTlsData;
 
 /*