diff --git a/jobs.c b/jobs.c
index 3ce9d2d..2cf53d2 100644
--- a/jobs.c
+++ b/jobs.c
@@ -97,6 +97,8 @@ extern int killpg __P((pid_t, int));
 #define MAX_JOBS_IN_ARRAY 128		/* testing */
 #endif
 
+#define PIDSTAT_TABLE_SZ 0x1000
+
 /* Flag values for second argument to delete_job */
 #define DEL_WARNSTOPPED		1	/* warn about deleting stopped jobs */
 #define DEL_NOBGPID		2	/* don't add pgrp leader to bgpids */
@@ -171,6 +173,7 @@ extern WORD_LIST *subst_assign_varlist;
 static struct jobstats zerojs = { -1L, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NO_JOB, NO_JOB, 0, 0 };
 struct jobstats js = { -1L, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NO_JOB, NO_JOB, 0, 0 };
 
+pidstat_index_t pidstat_table[PIDSTAT_TABLE_SZ];
 struct bgpids bgpids = { 0, 0, 0 };
 
 /* The array of known jobs. */
@@ -290,12 +293,14 @@ static void restore_sigint_handler __P((void));
 static void pipe_read __P((int *));
 #endif
 
-static struct pidstat *bgp_alloc __P((pid_t, int));
 static struct pidstat *bgp_add __P((pid_t, int));
+static pidstat_index_t bgp_alloc_index __P(());
 static int bgp_delete __P((pid_t));
 static void bgp_clear __P((void));
 static int bgp_search __P((pid_t));
-static void bgp_prune __P((void));
+
+static void pidstat_delete_index __P((pidstat_index_t));
+static pidstat_index_t *pidstat_bucket __P((pid_t));
 
 #if defined (ARRAY_VARS)
 static int *pstatuses;		/* list of pipeline statuses */
@@ -670,83 +675,137 @@ stop_pipeline (async, deferred)
   return (newjob ? i : js.j_current);
 }
 
-/* Functions to manage the list of exited background pids whose status has
-   been saved. */
+/* Functions to manage the list of exited background pids whose exit
+   status has been saved.
+
+   There are two access modes
+
+   - as a hashtable, on process id via pidstat_table
+
+   - as a circular buffer
+
+   The pidstat structures stored in a continuous region called
+   bgpids.storage - this is much faster when it needs to be cleared,
+   than having to free() each individually. Clearing happens very
+   frequently on fork and would otherwise cause unnecessary and
+   expensive copy-on-write.
+
+   If, for some reason, a pid is reused, its entry is wiped by
+   removing it from the hash table. The entry is only reused when the
+   circular buffer comes round, so this represents a small amount of
+   wasted space.
+*/
+
+static pidstat_index_t
+bgp_alloc_index()
+{
+  pidstat_index_t psi;
+
+  if (bgpids.head >= bgpids.nalloc) {
+    pidstat_index_t nsize;
+
+    if (bgpids.nalloc == 0) {
+      /* ensure the hash table is wiped */
+      for (psi = 0; psi < PIDSTAT_TABLE_SZ; ++psi)
+        pidstat_table[psi] = NO_PIDSTAT;
+
+      nsize = PIDSTAT_TABLE_SZ;
+    } else {
+      nsize = 2 * bgpids.nalloc;
+    }
+
+    if (bgpids.nalloc < js.c_childmax + PIDSTAT_TABLE_SZ) {
+      /* allocate some more entries */
+      bgpids.storage = (struct pidstat *)xrealloc (bgpids.storage, nsize * sizeof (struct pidstat));
+
+      for (psi = bgpids.nalloc; psi < nsize; ++psi)
+        bgpids.storage[psi].pid = NO_PID;
+
+      bgpids.nalloc = nsize;
+    } else {
+      /* don't allocate more, just wrap around the circular buffer */
+      bgpids.head = 0;
+    }
+  }
+
+  pidstat_delete_index(bgpids.head);
+
+  return bgpids.head++;
+}
 
 static struct pidstat *
-bgp_alloc (pid, status)
+bgp_add (pid, status)
      pid_t pid;
      int status;
 {
-  struct pidstat *ps;
+  pidstat_index_t *bucket = pidstat_bucket(pid);
+  pidstat_index_t psi = bgp_alloc_index();
+  struct pidstat *ps = &bgpids.storage[psi];
 
-  ps = (struct pidstat *)xmalloc (sizeof (struct pidstat));
   ps->pid = pid;
   ps->status = status;
-  ps->next = (struct pidstat *)0;
+
+  ps->bucket_next = *bucket;
+  ps->bucket_prev = NO_PIDSTAT;
+
+  if (ps->bucket_next != NO_PIDSTAT)
+    bgpids.storage[ps->bucket_next].bucket_prev = psi;
+
   return ps;
 }
 
-static struct pidstat *
-bgp_add (pid, status)
-     pid_t pid;
-     int status;
+static void
+pidstat_delete_index (psi)
+      pidstat_index_t psi;
 {
-  struct pidstat *ps;
+  struct pidstat *ps = &bgpids.storage[psi];
 
-  ps = bgp_alloc (pid, status);
+  if (ps->pid == NO_PID)
+    return;
 
-  if (bgpids.list == 0)
-    {
-      bgpids.list = bgpids.end = ps;
-      bgpids.npid = 0;			/* just to make sure */
-    }
+  if (ps->bucket_next != NO_PIDSTAT)
+    bgpids.storage[ps->bucket_next].bucket_prev = ps->bucket_prev;
+  if (ps->bucket_prev != NO_PIDSTAT)
+    bgpids.storage[ps->bucket_prev].bucket_next = ps->bucket_next;
   else
-    {
-      bgpids.end->next = ps;
-      bgpids.end = ps;
-    }
-  bgpids.npid++;
-
-  if (bgpids.npid > js.c_childmax)
-    bgp_prune ();
+    *pidstat_bucket(ps->pid) = ps->bucket_next;
+}
 
-  return ps;
+static pidstat_index_t*
+pidstat_bucket(pid)
+     pid_t pid;
+{
+  /* this simple hash function and prime constant come from Knuth,
+     close to an optimal modulus if we have 32-bits of integer
+     precision */
+  unsigned long hash = pid * 0x9e370001UL;
+  return &pidstat_table[hash % PIDSTAT_TABLE_SZ];
 }
 
 static int
 bgp_delete (pid)
      pid_t pid;
 {
-  struct pidstat *prev, *p;
+  pidstat_index_t psi;
+
+  /* ignore if job control disabled */
+  if (bgpids.storage == 0)
+    return 0;
 
-  for (prev = p = bgpids.list; p; prev = p, p = p->next)
-    if (p->pid == pid)
+  for (psi = *pidstat_bucket(pid); psi != NO_PIDSTAT; psi = bgpids.storage[psi].bucket_next)
+    if (bgpids.storage[psi].pid == pid)
       {
-	prev->next = p->next;	/* remove from list */
 	break;
       }
 
-  if (p == 0)
+  if (psi == NO_PIDSTAT)
     return 0;		/* not found */
 
+  pidstat_delete_index(psi);
+
 #if defined (DEBUG)
   itrace("bgp_delete: deleting %d", pid);
 #endif
-
-  /* Housekeeping in the border cases. */
-  if (p == bgpids.list)
-    bgpids.list = bgpids.list->next;
-  else if (p == bgpids.end)
-    bgpids.end = prev;
-
-  bgpids.npid--;
-  if (bgpids.npid == 0)
-    bgpids.list = bgpids.end = 0;
-  else if (bgpids.npid == 1)
-    bgpids.end = bgpids.list;		/* just to make sure */
-
-  free (p);
   return 1;
 }
 
@@ -754,16 +813,14 @@ bgp_delete (pid)
 static void
 bgp_clear ()
 {
-  struct pidstat *ps, *p;
+  if (bgpids.storage == 0)
+    return;
 
-  for (ps = bgpids.list; ps; )
-    {
-      p = ps;
-      ps = ps->next;
-      free (p);
-    }
-  bgpids.list = bgpids.end = 0;
-  bgpids.npid = 0;
+  free(bgpids.storage);
+
+  bgpids.storage = 0;
+  bgpids.nalloc = 0;
+  bgpids.head = 0;
 }
 
 /* Search for PID in the list of saved background pids; return its status if
@@ -772,29 +829,16 @@ static int
 bgp_search (pid)
      pid_t pid;
 {
-  struct pidstat *ps;
+  pidstat_index_t psi;
 
-  for (ps = bgpids.list ; ps; ps = ps->next)
-    if (ps->pid == pid)
-      return ps->status;
+  /* pretend empty if job control off, as hash table may not be cleared */
+  if (bgpids.storage == 0)
     return -1;
-}
-
-static void
-bgp_prune ()
-{
-  struct pidstat *ps;
 
-  if (bgpids.npid == 0 || bgpids.list == 0)
-    return;		/* just paranoia */
-
-  while (bgpids.npid > js.c_childmax)
-    {
-      ps = bgpids.list;
-      bgpids.list = bgpids.list->next;
-      free (ps);
-      bgpids.npid--;
-    }
+  for (psi = *pidstat_bucket(pid); psi != NO_PIDSTAT; psi = bgpids.storage[psi].bucket_next)
+    if (bgpids.storage[psi].pid == pid)
+      return bgpids.storage[psi].status;
+  return -1;
 }
 
 /* Reset the values of js.j_lastj and js.j_firstj after one or both have
@@ -866,6 +910,15 @@ cleanup_dead_jobs ()
 	delete_job (i, 0);
     }
 
+#if defined (PROCESS_SUBSTITUTION)
+  if (last_procsub_child && last_procsub_child->running == PS_DONE)
+    {
+      bgp_add (last_procsub_child->pid, process_exit_status (last_procsub_child->status));	/* XXX */
+      discard_pipeline (last_procsub_child);
+      last_procsub_child = (PROCESS *)NULL;
+    }
+#endif
+
 #if defined (COPROCESS_SUPPORT)
   coproc_reap ();
 #endif
@@ -3382,12 +3435,6 @@ itrace("waitchld: waitpid returns %d block = %d children_exited = %d", pid, bloc
 	    js.c_reaped++;
 	}
 
-#if defined (PROCESS_SUBSTITUTION)
-      /* XXX - should we make this unconditional and not depend on last procsub? */
-      if (child && child == last_procsub_child && child->running == PS_DONE)
-	bgp_add (child->pid, process_exit_status (child->status));	/* XXX */
-#endif	  
-
       if (job == NO_JOB)
 	continue;
 
@@ -3987,6 +4034,7 @@ initialize_job_control (force)
     js.c_childmax = getmaxchild ();
   if (js.c_childmax < 0)
     js.c_childmax = DEFAULT_CHILD_MAX;
+  js.c_childmax = 1000000;
 
   return job_control;
 }
diff --git a/jobs.h b/jobs.h
index abbeb7e..eb1f417 100644
--- a/jobs.h
+++ b/jobs.h
@@ -137,18 +137,22 @@ struct jobstats {
   JOB *j_lastasync;	/* last async job allocated by stop_pipeline */
 };
 
+typedef pid_t pidstat_index_t;
+
 struct pidstat {
- struct pidstat *next;
+  pidstat_index_t bucket_next;
+  pidstat_index_t bucket_prev;
   pid_t pid;
   int status;
 };
 
 struct bgpids {
-  struct pidstat *list;
-  struct pidstat *end;
-  int npid;
+  struct pidstat *storage;
+  pidstat_index_t head;
+  pidstat_index_t nalloc;
 };
 
+#define NO_PIDSTAT (pidstat_index_t)-1 /* Empty entry in PID status cache */
 #define NO_JOB  -1	/* An impossible job array index. */
 #define DUP_JOB -2	/* A possible return value for get_job_spec (). */
 #define BAD_JOBSPEC -3	/* Bad syntax for job spec. */
