From: Matthew Malcomson <[email protected]>

N.b. I've not tested this patch as thoroughly as my other upstream
libgomp patches.  I'm in the middle of doing so.  However I thought it
useful to get the patch visible early.

This patch goes on top of my earlier patch series (hence why sent "in
reply to" the last patch in that series).

This removes a large amount of overhead in starting a new parallel
region.  On one machine with 352 virtual cores this patch reduced the
cost of repeated parallel regions from ~90us to ~8us.

-------- 8< -------------- 8< ------------- 8< ----------

I'm working on reducing the overhead of the creation of a parallel
region.  My previous patches have focussed mostly on the barrier that is
a fundamental part of ending/starting a parallel region.

After those patches the majority of the remaining overhead from a
parallel region is inside gomp_team_start.  This function is executed by
the primary thread when all secondary threads are paused waiting to be
released from the barrier.  This means that it is highly performance
sensitive.

At high thread counts the largest cost in this function is the primary
thread iterating over all threads and setting variables on their
gomp_thread structure.

In an earlier patch I reduced the cost of this function by putting some
data in a shared location from where each of the secondary threads can
fetch it once they are released (hence parallelising the work).  This
patch attempts to do the same for all the data where it can be done.

The fields adjusted are as follows:
- nthr->ts.level
  This field is set to `team->prev_ts.level`, but the `if` clause that
  we are inside has already checked that `team->prev_ts.level == 0` and
  since we're re-using this thread it must have come from a non-nested
  team before (hence its `nthr->ts.level` must already be `1`).
- nthr->ts.active_level
  By a similar argument, this must already be `1` and
  `thr->ts.active_level` must also be `1`.
- Move `def_allocator`, `num_teams` and `team_num` into shared
  initialisation.  These are set the same on every thread, hence storing
  them once in the team data structure and having each thread take from
  there.
- Mark exiting threads instead of those continuing.
  The existing convention is that `gomp_thread_start` sets its own
  `thr->fn` to `NULL` and `gomp_team_start` sets the `nthr->fn` of each
  thread it wants to continue to the function it needs that thread to
  execute.  This means the primary thread needs to perform a write on
  each thread that is continuing.
  Here I switch the convention so that the primary thread sets
  `thr->exit` on each of the threads it wants to exit.  Those threads
  without this boolean flag set read the function that they are to
  execute from their team structure.  This means the primary thread
  writes once per exiting thread instead of once per reused thread.  In
  the case where the number of threads and affinity setup is not
  changing between parallel regions this means no work instead of O(N)
  work.
  There is still a cause in the main loop of `gomp_thread_start`
  checking for `thr->fn`, this is in order to let `gomp_pause_host` and
  `gomp_free_thread` continue to work as intended.  I could have changed
  those functions to adjust the `pool->last_team` instead of setting
  `thr->fn` but it didn't seem any better either way.
- Avoid re-calculation of places when relevant place ICV's are
  unchanged.  If we have calculated the places for each thread in a
  team, we're re-using that team, and all the inputs to that calculation
  are the same, then we can avoid the calculation and simply re-start
  the team.

libgomp/ChangeLog:

        * libgomp.h (struct gomp_team): New `ti` member.
        New `ppv` member.
        New `reused` boolean.
        (struct gomp_thread): New `exit` member.
        * team.c (gomp_thread_start): Read from `team->ti` for
        initialisation.
        (get_last_team): Set `team->reused` member when reusing.
        (gomp_new_team): Unset `team->reused` member when creating new
        team.
        (gomp_team_start): Populate `team->ti` instead of each thread
        individually, skip re-calculation of places on a reused team
        when inputs are the same, avoid reinitialisation of
        `nthr->ts.level` and `nthr->ts.active_level` when unnecessary.

Signed-off-by: Matthew Malcomson <[email protected]>
---
 libgomp/libgomp.h |  20 ++++++--
 libgomp/team.c    | 114 +++++++++++++++++++++++++++++++++++-----------
 2 files changed, 105 insertions(+), 29 deletions(-)

diff --git a/libgomp/libgomp.h b/libgomp/libgomp.h
index c854743c1c1..1dfdfae64df 100644
--- a/libgomp/libgomp.h
+++ b/libgomp/libgomp.h
@@ -870,9 +870,22 @@ struct gomp_team
      team very many times.  This allows each secondary thread to perform the
      re-initialisation instead of having the primary thread need to perform
      that initialisation.  */
-  struct gomp_task_icv task_icv_init;
-  struct gomp_task *task_parent_init;
-  struct gomp_taskgroup *task_taskgroup;
+  struct thread_init {
+    struct gomp_task_icv task_icv_init;
+    struct gomp_task *task_parent_init;
+    struct gomp_taskgroup *task_taskgroup;
+    uintptr_t def_allocator;
+    unsigned int num_teams;
+    unsigned int team_num;
+    void (*fn) (void *data);
+    void *data;
+  } ti;
+  struct prev_place_vars {
+    unsigned int place_partition_off;
+    unsigned int place_partition_len;
+    char bind;
+  } ppv;
+  bool reused;
 
   /* This array contains structures for implicit tasks.  */
   struct gomp_task implicit_task[];
@@ -886,6 +899,7 @@ struct gomp_thread
   /* This is the function that the thread should run upon launch.  */
   void (*fn) (void *data);
   void *data;
+  bool exit;
 
   /* This is the current team state for this thread.  The ts.team member
      is NULL only if the thread is idle.  */
diff --git a/libgomp/team.c b/libgomp/team.c
index 08c03fcf949..51758c46e36 100644
--- a/libgomp/team.c
+++ b/libgomp/team.c
@@ -109,6 +109,7 @@ gomp_thread_start (void *xdata)
   thr->place = data->place;
   thr->num_teams = data->num_teams;
   thr->team_num = data->team_num;
+  thr->exit = false;
 #ifdef GOMP_NEEDS_THREAD_HANDLE
   thr->handle = data->handle;
 #endif
@@ -143,6 +144,7 @@ gomp_thread_start (void *xdata)
          struct gomp_team *team = thr->ts.team;
          struct gomp_task *task = thr->task;
 
+         thr->fn = NULL;
          local_fn (local_data);
          gomp_team_barrier_wait_final (&team->barrier, thr->ts.team_id);
          gomp_finish_task (task);
@@ -155,14 +157,27 @@ gomp_thread_start (void *xdata)
             thread before it frees this team because it will synchronise with
             the primary thread in the next iteration of this loop (in
             `gomp_team_barrier_wait_final`).  */
-         if (!gomp_barrier_can_hold (&team->barrier) || thr->fn == NULL)
+         if (!gomp_barrier_can_hold (&team->barrier) || thr->exit)
            gomp_simple_barrier_wait (&pool->threads_dock);
 
          /* Early exit because if `thr->fn == NULL` then we can't guarantee
             the `thr->ts.team` hasn't been freed by the primary thread racing
             ahead.  Hence we don't want to write to it.  */
-         if (thr->fn == NULL)
+         if (thr->exit)
            break;
+
+         /* Usually `thr->fn` is not used or set.  Is used when secondary
+            threads are "set going" without being assigned to any team.
+            See `gomp_pause_host` and `gomp_free_thread`.
+            In this case we don't want to bother reading the team
+            initialisation things because they've not been set up for us.  */
+         if (thr->fn)
+           {
+             local_fn = thr->fn;
+             local_data = thr->data;
+             continue;
+           }
+
          team = thr->ts.team;
          thr->ts.work_share = &team->work_shares[0];
          thr->ts.last_work_share = NULL;
@@ -171,15 +186,16 @@ gomp_thread_start (void *xdata)
 #endif
          thr->ts.static_trip = 0;
          thr->task = &team->implicit_task[thr->ts.team_id];
-         gomp_init_task (thr->task, team->task_parent_init,
-                         &team->task_icv_init);
-         thr->task->taskgroup = team->task_taskgroup;
-
-         local_fn = thr->fn;
-         local_data = thr->data;
-         thr->fn = NULL;
+         gomp_init_task (thr->task, team->ti.task_parent_init,
+                         &team->ti.task_icv_init);
+         thr->task->taskgroup = team->ti.task_taskgroup;
+         thr->num_teams = team->ti.num_teams;
+         thr->team_num = team->ti.team_num;
+         thr->ts.def_allocator = team->ti.def_allocator;
+         local_fn = team->ti.fn;
+         local_data = team->ti.data;
        }
-      while (local_fn);
+      while (true);
     }
 
   gomp_sem_destroy (&thr->release);
@@ -208,6 +224,7 @@ get_last_team (unsigned nthreads)
                       "prev_barrier not within cached team: %p != %p",
                       pool->prev_barrier, &pool->last_team->barrier);
          pool->last_team = NULL;
+         last_team->reused = true;
          return last_team;
        }
     }
@@ -241,6 +258,7 @@ gomp_new_team (unsigned nthreads)
       gomp_mutex_init (&team->task_lock);
 
       team->nthreads = nthreads;
+      team->reused = false;
     }
 
   team->work_share_chunk = 8;
@@ -454,15 +472,6 @@ gomp_team_start (void (*fn) (void *), void *data, unsigned 
nthreads,
   thr->task->taskgroup = taskgroup;
   team->implicit_task[0].icv.nthreads_var = nthreads_var;
   team->implicit_task[0].icv.bind_var = bind_var;
-  /* Copy entirely rather than copy pointer so that we have an immutable copy
-     for the secondary threads to take from for the time-frame that said
-     secondary threads are executing on.  Can't copy from
-     `team->implicit_task[0].icv` because primary thread could have adjusted
-     its per-task ICV by the time the secondary thread gets around to
-     initialising things.   */
-  team->task_icv_init = team->implicit_task[0].icv;
-  team->task_parent_init = task;
-  team->task_taskgroup = taskgroup;
 
   if (nthreads == 1)
     return;
@@ -549,6 +558,41 @@ gomp_team_start (void (*fn) (void *), void *data, unsigned 
nthreads,
                   team->prev_ts.team_id);
       old_threads_used = pool->threads_used;
 
+      /* Copy entirely rather than copy pointer so that we have an immutable
+        copy for the secondary threads to take from for the time-frame that
+        said secondary threads are executing on.  Can't copy from
+        `team->implicit_task[0].icv` because primary thread could have adjusted
+        its per-task ICV by the time the secondary thread gets around to
+        initialising things.   */
+      team->ti.task_icv_init = team->implicit_task[0].icv;
+      team->ti.task_parent_init = task;
+      team->ti.task_taskgroup = taskgroup;
+      team->ti.num_teams = thr->num_teams;
+      team->ti.team_num = thr->team_num;
+      team->ti.fn = fn;
+      team->ti.data = data;
+      team->ti.def_allocator = thr->ts.def_allocator;
+
+      if (team->reused
+         && team->ppv.place_partition_off == thr->ts.place_partition_off
+         && team->ppv.place_partition_len == thr->ts.place_partition_len
+         && team->ppv.bind == bind)
+       {
+         gomp_assert (
+           nthreads == old_threads_used,
+           "odd number of threads when re-using team: nthreads = %u "
+           "old_threads_used = %u\n",
+           nthreads, old_threads_used);
+         goto do_release;
+       }
+      else
+       {
+         team->ppv.place_partition_off = thr->ts.place_partition_off;
+         team->ppv.place_partition_len = thr->ts.place_partition_len;
+         team->ppv.bind = bind;
+       }
+
+
       if (nthreads == old_threads_used)
        {
          n = nthreads;
@@ -712,6 +756,8 @@ gomp_team_start (void (*fn) (void *), void *data, unsigned 
nthreads,
                              pool->threads[j]->data = affinity_thr[l];
                              affinity_thr[l] = pool->threads[j];
                            }
+                         else
+                           pool->threads[j]->exit = true;
                          pool->threads[j] = NULL;
                        }
                      if (nthreads > old_threads_used)
@@ -752,21 +798,34 @@ gomp_team_start (void (*fn) (void *), void *data, 
unsigned nthreads,
            nthr = pool->threads[i];
          __atomic_store_n (&nthr->ts.team, team, MEMMODEL_RELEASE);
          nthr->ts.team_id = i;
-         nthr->ts.level = team->prev_ts.level + 1;
-         nthr->ts.active_level = thr->ts.active_level;
+         gomp_assert (
+           nthr->ts.level == 1 && team->prev_ts.level == 0,
+           "Level indicators surprising: nthr: %u, team->prev_ts: %u\n",
+           nthr->ts.level, team->prev_ts.level);
+         gomp_assert (
+           nthr->ts.active_level == 1 && thr->ts.active_level == 1,
+           "active_level indicators surprising: nthr: %u, thr: %u\n",
+           nthr->ts.active_level, thr->ts.active_level);
          nthr->ts.place_partition_off = place_partition_off;
          nthr->ts.place_partition_len = place_partition_len;
-         nthr->ts.def_allocator = thr->ts.def_allocator;
-         nthr->num_teams = thr->num_teams;
-         nthr->team_num = thr->team_num;
          nthr->place = place;
-         nthr->fn = fn;
-         nthr->data = data;
          team->ordered_release[i] = &nthr->release;
        }
 
       if (__builtin_expect (affinity_thr != NULL, 0))
        {
+         /* Mark each of the threads that are exiting as such.  Hence in
+            `gomp_thread_start` they exit their main loop.  */
+         for (unsigned l = 0; l < team->prev_ts.place_partition_len; l++)
+           {
+             struct gomp_thread *t = affinity_thr[l];
+             while (t)
+               {
+                 struct gomp_thread *next = (struct gomp_thread *) t->data;
+                 t->exit = true;
+                 t = next;
+               }
+           }
          /* If AFFINITY_THR is non-NULL just because we had to
             permute some threads in the pool, but we've managed
             to find exactly as many old threads as we'd find
@@ -857,6 +916,9 @@ gomp_team_start (void (*fn) (void *), void *data, unsigned 
nthreads,
                }
            }
        }
+      else
+       for (unsigned j = n; j < old_threads_used; j++)
+         pool->threads[j]->exit = true;
 
       if (i == nthreads)
        {
-- 
2.43.0

Reply via email to