Mirror of the gdb-patches mailing list
 help / color / mirror / Atom feed
From: Simon Marchi <simon.marchi@polymtl.ca>
To: Aditya Vidyadhar Kamath <akamath996@gmail.com>,
	ulrich.weigand@de.ibm.com,  tom@tromey.com
Cc: gdb-patches@sourceware.org, Aditya.Kamath1@ibm.com,
	sangamesh.swamy@in.ibm.com
Subject: Re: [PATCH v2] Speed up next/step while debugging multithreaded programs on AIX.
Date: Thu, 10 Sep 2026 09:25:49 -0400	[thread overview]
Message-ID: <13694747-63ee-44a0-ae1a-fdd1527dd009@polymtl.ca> (raw)
In-Reply-To: <20260910101643.85955-2-akamath996@gmail.com>



On 2026-09-10 06:16, 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

Seems to be missing a word after "simple".  Statements?

> 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.

I mean, it *could* change, you could do "next" over a function that
spawns a thread, a background thread (one other than the one you step)
could spawn a thread, another thread could have exited, etc.

> So what I changed in this patch 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).

It's unrelated to this patch, but I happen to be looking at aix-thread.c
because of it and I must ask if these things are still relevant, given
the AIX versions and configurations you support nowadays:

1. #if !HAVE_DECL_GETTHRDS
   extern int getthrds (pid_t, struct thrdsinfo64 *, int, tid_t *, int);
   #endif

   The commit message that added this is:

     On newer versions of AIX (6.x and later), this function is actually
     declared in procinfo.h, thus causing a compilation warning when we
     re-declare it ourselves. This patch adds a configure check for that
     function allowing us to declare the function only if the declaration
     isn't already present in one of procinfo system header.

2. /* In AIX 5.1, functions use pthdb_tid_t instead of tid_t.  */
   #ifndef PTHDB_VERSION_3
   #define pthdb_tid_t	tid_t
   #endif

3. #ifdef HAVE_PTRACE64
   # define ptracex(request, pid, addr, data, buf) \
         ptrace64 (request, pid, addr, data, buf)
   #endif

> 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.

Ulrich already pointed out that you could have one thread appear and one
disappear, and the count will not change.

> 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.

So yeah, what about doing "next" over a call that spawns a thread, or a
background thread existing while you do a next?

> 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().

This sounds wrong, as a regular breakpoint is also (typically) reported
as a TARGET_WAITKIND_STOPPED with a SIGTRAP.  In the implementation, you
also check for data->last_resume_step, which is not mentioned here.

> 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.
> 
> ==================
> 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 | 153 ++++++++++++++++++++++++++++++++++++++++-------
>  1 file changed, 130 insertions(+), 23 deletions(-)
> 
> diff --git a/gdb/aix-thread.c b/gdb/aix-thread.c
> index 5ac71d22237..3370736810a 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), last_resume_step (0)
> +  {}
> +
>    /* Whether the current application is debuggable by pthdb.  */
>    int pd_able;
>  
> @@ -192,6 +197,19 @@ 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;
> +
> +  /* Set to non-zero by resume() when the resume was a software
> +     single-step, i.e. single-step breakpoints were inserted for the
> +     current thread before the inferior was set running.  Cleared to
> +     zero for any other kind of resume.  Used by wait() to distinguish
> +     a SIGTRAP from a completed single-step from one caused by a
> +     breakpoint, since both look identical at the signal level.  */
> +  int last_resume_step;
>  };
>  
>  /* Key to our per-inferior data.  */
> @@ -733,6 +751,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.  */

No need to document what the original code did.  Just keep the first
sentence.

> +#define GETTHRDS_BATCH 64

Prefer:

constexpr int 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.  */

This predates your patch, but is the comment for the get_signaled_thread
function accurate?  It says it looks for a thread that has stopped on a
SIGTRAP signal, but the implementation seems to look for any signal, not
just SIGTRAP.

> @@ -740,20 +763,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)

Prefer declaring the variable in the while statement, and separating the
comparison from the assignment.  This should work:

  while (int count = getthrds (pid, thrinf, sizeof (thrinf[0]), &ktid,
			       GETTHRDS_BATCH));
	 count > 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;

Declare variable `i` in the for loop.

Simon

  parent reply	other threads:[~2026-09-10 13:26 UTC|newest]

Thread overview: 5+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-09-10 10:16 Aditya Vidyadhar Kamath
2026-09-10 13:14 ` Ulrich Weigand
2026-09-10 13:45   ` Simon Marchi
2026-09-10 13:25 ` Simon Marchi [this message]
2026-09-10 17:32 ` Abhay Kandpal

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=13694747-63ee-44a0-ae1a-fdd1527dd009@polymtl.ca \
    --to=simon.marchi@polymtl.ca \
    --cc=Aditya.Kamath1@ibm.com \
    --cc=akamath996@gmail.com \
    --cc=gdb-patches@sourceware.org \
    --cc=sangamesh.swamy@in.ibm.com \
    --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