Mirror of the gdb-patches mailing list
 help / color / mirror / Atom feed
From: Abhay Kandpal <abhay@linux.ibm.com>
To: Aditya Vidyadhar Kamath <akamath996@gmail.com>,
	ulrich.weigand@de.ibm.com,  simon.marchi@polymtl.ca,
	tom@tromey.com
Cc: gdb-patches@sourceware.org, Aditya.Kamath1@ibm.com,
	sangamesh.swamy@in.ibm.com
Subject: Re: [PATCH v1][RFC] Speed up next/step while debugging multithreaded programs on AIX.
Date: Wed, 9 Sep 2026 23:28:53 +0530	[thread overview]
Message-ID: <b78628da-05fa-4303-b824-fe6c993741c9@linux.ibm.com> (raw)
In-Reply-To: <20260909054306.73173-2-akamath996@gmail.com>

[-- Attachment #1: Type: text/plain, Size: 15289 bytes --]

Hi Aditya,

On 09/09/26 11:13, Aditya Vidyadhar Kamath wrote:
> From: Aditya Vidyadhar Kamath<aditya.kamath1@ibm.com>
>
> During go compiler debugging on AIX my collegue and I noticed that
> the next command was pretty slow compared to Linux - around 7 seconds
> per step when program has atleast 3 or more threads running. A simple
> walk through a function took around a minute. In compiler codes this is
> slow.
>
> TO measure this properly I wrote a benchmark pasted below. The program spawns
> 20 worker threads , then stops inside a function with 10 simple
> that GDB steps over one by one. The GDB batch script breaks at that
> function and issues 10 next commands and then quits. Timing the whole
> run gives a fair picture of how much overhead each `next` carries.
> Both the .c file and batch file are pasted below.
>
> Before this patch, on AIX 7.3 ppc64 with 20 background threads:
>
> real    1m10.05s
> user    0m17.59s
> sys     0m20.43s
>
> That is roughly 7 seconds per `next` step.
>
> After this patch:
>
> real    0m20.37s
> user    0m5.94s
> sys     0m6.53s
>
> About 2 seconds per step.  3.5x faster for the same workload.
>
> Now why was it slow?
>
> Every time the inferior stops -- even for a single-step trap from next
> - the AIX thread layer called pd_update(), which did two very expensive
> things unconditionally:
>
> 1. pthdb_session_update ()
>
> This function walks through all of the pthread library's internal
> data structures inside the inferior process, reading them out via
> pdc_read_data() callbacks. With 20 threads that is over 300 memory
> reads from the inferior on every single stop, even when absolutely
> nothing about the threads has changed.
>
> 2. sync_threadlists()
>
> This follows the session update with a full rebuild of GDB's thread
> list: iterate every thread via pthdb_pthread(), call
> pthdb_pthread_tid() for each one, allocate two sorted arrays, diff
> them against GDB's internal list, and reconcile the differences.
> Again, all of this on every stop, even when the thread population
> has not changed at all since the last stop.
>
> On top of that, get_signaled_thread() was calling getthrds() asking for
> one kernel thread descriptor at a time, which costs one syscall per
> thread in the process.
>
> None of this work is wrong but it is needed when threads actually come
> and go.  But during a next or step sequence through a function, the
> thread list does not change. Paying for a full rescan on every stop is
> something not needed I think.
>
> So what changes this patch does is:
>
> 1. Batch getthrds() calls get_signaled_thread ()
>
> The original code passed 1 as the count argument to getthrds(), so
> it did one syscall per kernel thread to find the one that stopped.
> Changed to fetch 64 descriptors per call and scan the batch in a
> loop. This alone cuts the getthrds() cost from O(N) syscalls to
> O(N/64).
>
> 2. Track the thread count - aix_thread_variables, pd_activate
>
> Added a last_thread_count field to aix_thread_variables.  It records
> how many threads libpthdebug reported the last time a full sync was
> done. Initialised to -1 in the constructor so the very
> first stop always does a full sync. Also reset to -1 in pd_activate()
> so that whenever a new debug session is opened the first stop is
> always fully synced regardless of the stop kind.
>
> 3. Skip the sync on clean step stops
>
> pd_update() now takes a bool step_stop parameter.  When that is true
> and last_thread_count is not -1, it skips pthdb_session_update() and
> sync_threadlists() entirely.
>
> When a sync is needed (not a step stop, or first stop after
> activation), a new helper count_pthdb_threads() does a lightweight
> pass through pthdb_pthread() just to count threads. It only calls
> sync_threadlists() if that count differs from last_thread_count.
> This avoids the expensive pthdb_pthread_tid() calls and array
> allocations on stops where the thread count has not changed.
>
> 4. Detect step stops in wait() and pass the flag down
>
> wait() already has the stop kind and signal number before it calls
> pd_update(). A TARGET_WAITKIND_STOPPED with GDB_SIGNAL_TRAP is what
> the kernel reports for a completed single-step. wait() now computes
> step_stop from those two fields and passes it to pd_update().
>
> int current_count = count_pthdb_threads (data);
>            if (current_count != data->last_thread_count)
>
> This makes sure we never miss any genuinely coming new threads and are
> in sync.
>
> So these are the  things I thought and did. Let me know what you think.
>
> ==================
> Benchmark program (bench_next.c):
>
>      /* Spawns NUM_THREADS worker threads that spin, then steps through
>         10 assignments in do_steps() so GDB can time each next.  */
>      #include <stdio.h>
>      #include <stdlib.h>
>      #include <pthread.h>
>      #include <unistd.h>
>
>      #ifndef NUM_THREADS
>      #define NUM_THREADS 20
>      #endif
>
>      static volatile int keep_running = 1;
>
>      static void *
>      worker (void *arg)
>      {
>        while (keep_running)
>          sched_yield ();
>        return NULL;
>      }
>
>      void
>      do_steps (void)
>      {
>        volatile int a = 1;
>        volatile int b = 2;
>        volatile int c = a + b;
>        volatile int d = c * 2;
>        volatile int e = d - a;
>        volatile int f = e + c;
>        volatile int g = f / 2;
>        volatile int h = g + 1;
>        volatile int i2 = h * h;
>        volatile int j = i2 - b;
>        (void)j;
>      }
>
>      int
>      main (void)
>      {
>        pthread_t threads[NUM_THREADS];
>        int i;
>
>        for (i = 0; i < NUM_THREADS; i++)
>          pthread_create (&threads[i], NULL, worker, NULL);
>
>        do_steps ();
>
>        keep_running = 0;
>        for (i = 0; i < NUM_THREADS; i++)
>          pthread_join (threads[i], NULL);
>
>        return 0;
>      }
>
> ====================
> Build done with
> gcc -O0 -g -gdwarf -maix64 -DNUM_THREADS=20 -o bench_next_bin bench_next.c -lpthread
> =====================
> GDB batch script (bench_next.gdb):
>
>      set pagination off
>      set confirm off
>
>      break do_steps
>      run
>
>      next
>      next
>      next
>      next
>      next
>      next
>      next
>      next
>      next
>      next
>
>      quit
> ========================
> Then run,
> time gdb -batch -x bench_next.gdb ./bench_next_bin
> ---
>   gdb/aix-thread.c | 117 +++++++++++++++++++++++++++++++++++++----------
>   1 file changed, 94 insertions(+), 23 deletions(-)
>
> diff --git a/gdb/aix-thread.c b/gdb/aix-thread.c
> index 5ac71d22237..5c43e1130e4 100644
> --- a/gdb/aix-thread.c
> +++ b/gdb/aix-thread.c
> @@ -173,6 +173,11 @@ static pthdb_callbacks_t pd_callbacks = {
>   /* Aix variable structure.  */
>   struct aix_thread_variables
>   {
> +  aix_thread_variables ()
> +    : pd_able (0), pd_active (0), pd_session (0), pd_brk_addr (0),
> +      arch64 (0), last_thread_count (-1)
> +  {}
> +
>     /* Whether the current application is debuggable by pthdb.  */
>     int pd_able;
>   
> @@ -192,6 +197,11 @@ struct aix_thread_variables
>   
>     /* Describes the number of thread exit events reported.  */
>     std::unordered_set<pthdb_pthread_t> exited_threads;
> +
> +  /* Last known libpthdebug thread count.  Used to skip sync_threadlists()
> +     when the thread population has not changed since the previous stop,
> +     which is the common case during stepping.  -1 means unknown/force sync.  */
> +  int last_thread_count;
>   };
>   
>   /* Key to our per-inferior data.  */
> @@ -733,6 +743,11 @@ state2str (pthdb_state_t state)
>       }
>   }
>   
> +/* Number of thread descriptors to fetch per getthrds() call.  The original
> +   code fetched one at a time, which costs one syscall per thread.  Fetching
> +   in batches reduces that to one syscall per GETTHRDS_BATCH threads.  */
> +#define GETTHRDS_BATCH 64
> +
>   /* Search through the list of all kernel threads for the thread
>      that has stopped on a SIGTRAP signal, and return its TID.
>      Return 0 if none found.  */
> @@ -740,20 +755,18 @@ state2str (pthdb_state_t state)
>   static pthdb_tid_t
>   get_signaled_thread (int pid)
>   {
> -  struct thrdsinfo64 thrinf;
> +  struct thrdsinfo64 thrinf[GETTHRDS_BATCH];
>     tid_t ktid = 0;
> +  int count, i;
>   
> -  while (1)
> +  while ((count = getthrds (pid, thrinf, sizeof (thrinf[0]),
> +			    &ktid, GETTHRDS_BATCH)) > 0)
>       {
> -      if (getthrds (pid, &thrinf,
> -		    sizeof (thrinf), &ktid, 1) != 1)
> -	break;
> -
>         /* We also need to keep in mind Trap and interrupt or any
>   	 signal that needs to be handled in pd_update ().  */
> -
> -      if (thrinf.ti_cursig)
> -	return thrinf.ti_tid;
> +      for (i = 0; i< count; i++) + if (thrinf[i].ti_cursig) + return thrinf[i].ti_tid; } 
> /* Didn't find any thread stopped on a SIGTRAP signal. */ @@ -861,13 
> +874,39 @@ sync_threadlists (pid_t pid) } } +/* Count the number of 
> live pthreads visible to libpthdebug. + Used before sync_threadlists() 
> to check whether the thread population + has changed since the last 
> stop. */ + +static int +count_pthdb_threads (struct 
> aix_thread_variables *data) +{ + pthdb_pthread_t pdtid; + int cmd, n = 
> 0; + + for (cmd = PTHDB_LIST_FIRST;; cmd = PTHDB_LIST_NEXT) + { + int 
> status = pthdb_pthread (data->pd_session, &pdtid, cmd);
> +      if (status != PTHDB_SUCCESS || pdtid == PTHDB_INVALID_PTHREAD)
> +	break;
> +      n++;
> +    }
> +  return n;
> +}
> +
>   /* Synchronize libpthdebug's state with the inferior and with GDB,
>      generate a composite process/thread <pid> for the current thread,
> -   Return the ptid of the event thread if one can be found, else
> -   return a pid-only ptid with PID.  */
> +   return the ptid of the event thread if one can be found, else
> +   return a pid-only ptid with PID.
> +
> +   STEP_STOP should be set when the stop is a single-step SIGTRAP.
> +   In that case, if the thread list was already synced at a previous
> +   stop, both pthdb_session_update() and sync_threadlists() are skipped.
> +   Those two calls dominate the cost of every stop in a multithreaded
> +   program, so skipping them during stepping gives a large speedup.  */
>   
>   static ptid_t
> -pd_update (pid_t pid)
> +pd_update (pid_t pid, bool step_stop = false)
>   {
>     int status;
>     ptid_t ptid;
> @@ -880,15 +919,36 @@ pd_update (pid_t pid)
>     if (!data->pd_active)
>       return ptid_t (pid);
>   
> -  status = pthdb_session_update (data->pd_session);
> -  if (status != PTHDB_SUCCESS)
> -    return ptid_t (pid);
> -
> -  /* Attempt to sync_threadlists () only when debugging object files
> -     and not core files since list of threads never change for core
> -     files.  */
> -  if (target_has_execution ())
> -    sync_threadlists (pid);
> +  /* Skip the session update and thread list sync when stepping.
> +     pthdb_session_update() reads through all pthread data structures in
> +     the inferior on every call, and sync_threadlists() follows it with
> +     O(N) kernel calls per thread.  During a next/step sequence the thread
> +     list does not change, so both calls can be omitted as long as we have
> +     done at least one full sync since activation (last_thread_count >= 0).
> +     Any other stop kind (breakpoints, real signals) always does a full
> +     sync because threads may have been created or destroyed.  */
> +  bool need_sync = !step_stop || (data->last_thread_count < 0);
> +
> +  if (need_sync)
> +    {
> +      status = pthdb_session_update (data->pd_session);
> +      if (status != PTHDB_SUCCESS)
> +	return ptid_t (pid);
> +
> +      /* Attempt to sync_threadlists() only when debugging object files
> +	 and not core files since the list of threads never changes for
> +	 core files.  Even for live inferiors, skip the sync when the
> +	 thread count matches the previous stop.  */
> +      if (target_has_execution ())
> +	{
> +	  int current_count = count_pthdb_threads (data);
> +	  if (current_count != data->last_thread_count)
> +	    {
> +	      sync_threadlists (pid);
> +	      data->last_thread_count = current_count;
> +	    }
> +	}
> +    }
>   
>     /* Define "current thread" as one that just received a trap signal.  */
>   
> @@ -921,7 +981,12 @@ pd_activate (pid_t pid)
>   			       PTHDB_FLAG_REGS, &pd_callbacks,
>   			       &data->pd_session);
>     if (status == PTHDB_SUCCESS)
> -    data->pd_active = 1;
> +    {
> +      data->pd_active = 1;
> +      /* Reset last_thread_count so that the very first stop after
> +	 activation always does a full sync_threadlists() pass.  */
> +      data->last_thread_count = -1;
> +    }
>   }
>   
>   /* AIX implementation of update_thread_list.  */
> @@ -1125,7 +1190,13 @@ aix_thread_target::wait (ptid_t ptid, struct target_waitstatus *status,
>   	pd_activate (ptid.pid ());
>       }
>   
> -  return pd_update (ptid.pid ());
> +  /* Tell pd_update() whether this is a single-step SIGTRAP so it can
> +     skip the expensive pthdb_session_update() and sync_threadlists()
> +     calls.  Breakpoints, real signals and other stop kinds always get
> +     the full sync.  */
> +  bool step_stop = (status->kind () == TARGET_WAITKIND_STOPPED
> +		    && status->sig () == GDB_SIGNAL_TRAP);
> +  return pd_update (ptid.pid (), step_stop);

Doesn't a breakpoint stop also arrive as TARGET_WAITKIND_STOPPED with GDB_SIGNAL_TRAP?
get_signaled_thread() already looks for the thread stopped on SIGTRAP to find the
current thread, and that has to work at breakpoint stops too, so I would expect
a breakpoint hit to look the same as a single-step here.

If so, breakpoint stops also get step_stop = true and skip the sync, which is the
opposite of what the commit message says:

"Any other stop kind (breakpoints, real signals) always does a full sync
because threads may have been created or destroyed."

The last_thread_count < 0 check only covers the first stop after pd_activate().
After that first sync, last_thread_count is >= 0 for the rest of the session
and need_sync becomes just !step_stop.  So from the second stop onwards
a breakpoint hit skips the sync, and any threads created since the previous
stop are missed.

The benchmark won't show this: all 20 threads are created before do_steps()
and there is only one breakpoint, so the thread list never changes after the
first stop.

resume() is already told whether GDB asked for a single step, and it already
has the aix_thread_variables pointer in hand.  Could you save the flag there
and use it here instead of inferring it from the signal?

/* aix_thread_target::resume (), after the existing
data = get_thread_data_helper_for_ptid (ptid); */
data->last_resume_step = step;

/* in wait () */
bool step_stop = (data->last_resume_step
&& status->kind () == TARGET_WAITKIND_STOPPED
&& status->sig () == GDB_SIGNAL_TRAP);

That would also handle "next" over a function call, where GDB puts a temporary
breakpoint at the return address and continues rather than single-stepping
through the callee.  resume() is called with step = 0 in that case, so the
stop would correctly get a full sync even though the user typed "next".

Regards,
Abhay

>   }
>   
>   /* Supply AIX altivec registers, both 64 and 32 bit.  */

[-- Attachment #2: Type: text/html, Size: 16017 bytes --]

  reply	other threads:[~2026-09-09 17:59 UTC|newest]

Thread overview: 3+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-09-09  5:43 Aditya Vidyadhar Kamath
2026-09-09 17:58 ` Abhay Kandpal [this message]
2026-09-10 10:15   ` Aditya Kamath

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=b78628da-05fa-4303-b824-fe6c993741c9@linux.ibm.com \
    --to=abhay@linux.ibm.com \
    --cc=Aditya.Kamath1@ibm.com \
    --cc=akamath996@gmail.com \
    --cc=gdb-patches@sourceware.org \
    --cc=sangamesh.swamy@in.ibm.com \
    --cc=simon.marchi@polymtl.ca \
    --cc=tom@tromey.com \
    --cc=ulrich.weigand@de.ibm.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox