diff --git a/src/backend/access/transam/README b/src/backend/access/transam/README
index 22c8ae9755..0f61f814c0 100644
--- a/src/backend/access/transam/README
+++ b/src/backend/access/transam/README
@@ -376,7 +376,7 @@ commit state is dependent on the commit status of each and every ancestor
 transaction.
 
 The "subtransaction parent" (pg_subtrans) mechanism records, for each
-transaction with an XID, the TransactionId of its parent transaction.  This
+transaction with an XID, the TransactionId of its top-level transaction.  This
 information is stored as soon as the subtransaction is assigned an XID.
 Top-level transactions do not have a parent, so they leave their pg_subtrans
 entries set to the default value of zero (InvalidTransactionId).
diff --git a/src/backend/access/transam/subtrans.c b/src/backend/access/transam/subtrans.c
index 66d3548155..ac92588b40 100644
--- a/src/backend/access/transam/subtrans.c
+++ b/src/backend/access/transam/subtrans.c
@@ -5,10 +5,10 @@
  *
  * The pg_subtrans manager is a pg_xact-like manager that stores the parent
  * transaction Id for each transaction.  It is a fundamental part of the
- * nested transactions implementation.  A main transaction has a parent
- * of InvalidTransactionId, and each subtransaction has its immediate parent.
- * The tree can easily be walked from child to parent, but not in the
- * opposite direction.
+ * nested transactions implementation.
+ *
+ * A top-level xact's entry in subtrans is always InvalidTransactionId.
+ * A subxact's entry in subtrans always points direct to the topxid.
  *
  * This code is based on xact.c, but the robustness requirements
  * are completely different from pg_xact, because we only need to remember
@@ -105,7 +105,7 @@ SubTransSetParent(TransactionId xid, TransactionId parent)
 /*
  * Interrogate the parent of a transaction in the subtrans log.
  */
-TransactionId
+static inline TransactionId
 SubTransGetParent(TransactionId xid)
 {
 	int			pageno = TransactionIdToPage(xid);
@@ -137,46 +137,49 @@ SubTransGetParent(TransactionId xid)
 /*
  * SubTransGetTopmostTransaction
  *
- * Returns the topmost transaction of the given transaction id.
+ * Returns the topmost transaction of the given transaction id, if one has
+ * been recorded in pg_subtrans.
  *
- * Because we cannot look back further than TransactionXmin, it is possible
- * that this function will lie and return an intermediate subtransaction ID
+ * Since we cannot look back further than TransactionXmin, it is possible
+ * that this function will lie and return the input xid unchanged
  * instead of the true topmost parent ID.  This is OK, because in practice
  * we only care about detecting whether the topmost parent is still running
  * or is part of a current snapshot's list of still-running transactions.
  * Therefore, any XID before TransactionXmin is as good as any other.
+ *
+ * There is no longer a loop here since all subxids point directly to their
+ * topxids, hence we need only call SubTransGetParent() once.
  */
 TransactionId
 SubTransGetTopmostTransaction(TransactionId xid)
 {
-	TransactionId parentXid = xid,
-				previousXid = xid;
+	TransactionId parentXid;
 
 	/* Can't ask about stuff that might not be around anymore */
 	Assert(TransactionIdFollowsOrEquals(xid, TransactionXmin));
 
-	while (TransactionIdIsValid(parentXid))
-	{
-		previousXid = parentXid;
-		if (TransactionIdPrecedes(parentXid, TransactionXmin))
-			break;
-		parentXid = SubTransGetParent(parentXid);
-
-		/*
-		 * By convention the parent xid gets allocated first, so should always
-		 * precede the child xid. Anything else points to a corrupted data
-		 * structure that could lead to an infinite loop, so exit.
-		 */
-		if (!TransactionIdPrecedes(parentXid, previousXid))
-			elog(ERROR, "pg_subtrans contains invalid entry: xid %u points to parent xid %u",
-				 previousXid, parentXid);
-	}
+	if (!TransactionIdIsValid(xid))
+		return xid;
 
-	Assert(TransactionIdIsValid(previousXid));
+	parentXid = SubTransGetParent(xid);
 
-	return previousXid;
-}
+	if (!TransactionIdIsValid(parentXid))
+		return xid;
 
+	/*
+	 * By convention the parent xid gets allocated first, so should always
+	 * precede the child xid. Anything else points to a corrupted data
+	 * structure that could lead to an infinite loop, so exit.
+	 */
+	if (!TransactionIdPrecedes(parentXid, xid))
+		elog(ERROR, "pg_subtrans contains invalid entry: xid %u points to parent xid %u",
+			 xid, parentXid);
+
+	if (TransactionIdPrecedes(parentXid, TransactionXmin))
+		return xid;
+
+	return parentXid;
+}
 
 /*
  * Initialization of shared memory for SUBTRANS
diff --git a/src/backend/access/transam/transam.c b/src/backend/access/transam/transam.c
index 5865810135..66299454c3 100644
--- a/src/backend/access/transam/transam.c
+++ b/src/backend/access/transam/transam.c
@@ -135,7 +135,12 @@ TransactionIdDidCommit(TransactionId transactionId)
 		return true;
 
 	/*
-	 * If it's marked subcommitted, we have to check the parent recursively.
+	 * If it's marked subcommitted, we know that there is or was a commit in
+	 * progress for the topxid. If we simply wait for the status to change to
+	 * committed we might wait forever, so we have to check the topxid status
+	 * and for similar reasons can't wait there either, even though that would
+	 * be more useful in the common case.
+	 *
 	 * However, if it's older than TransactionXmin, we can't look at
 	 * pg_subtrans; instead assume that the parent crashed without cleaning up
 	 * its children.
@@ -147,6 +152,11 @@ TransactionIdDidCommit(TransactionId transactionId)
 	 * StartupSUBTRANS() has ensured that any missing information will be
 	 * zeroed.  Since this case should not happen under normal conditions, it
 	 * seems reasonable to emit a WARNING for it.
+	 *
+	 * This implementation is new for PG16, since SubTransGetParent() is now
+	 * private, we make one call to SubTransGetTopmostTransaction() rather than
+	 * a recursive call. The design pattern is same for TransactionIdDidCommit()
+	 * and for TransactionIdDidAbort().
 	 */
 	if (xidstatus == TRANSACTION_STATUS_SUB_COMMITTED)
 	{
@@ -154,14 +164,30 @@ TransactionIdDidCommit(TransactionId transactionId)
 
 		if (TransactionIdPrecedes(transactionId, TransactionXmin))
 			return false;
-		parentXid = SubTransGetParent(transactionId);
-		if (!TransactionIdIsValid(parentXid))
+		parentXid = SubTransGetTopmostTransaction(transactionId);
+		if (parentXid == transactionId)
 		{
 			elog(WARNING, "no pg_subtrans entry for subcommitted XID %u",
 				 transactionId);
 			return false;
 		}
-		return TransactionIdDidCommit(parentXid);
+
+		/*
+		 * Get the topxid status without delay, even though in the typical case
+		 * we are racing against the final commit.
+		 */
+		xidstatus = TransactionLogFetch(parentXid);
+
+		/*
+		 * Topxid should never show as subcommitted; see clog.c
+		 */
+		Assert(xidstatus != TRANSACTION_STATUS_SUB_COMMITTED);
+
+		/*
+		 * If it's marked committed, it's committed.
+		 */
+		if (xidstatus == TRANSACTION_STATUS_COMMITTED)
+			return true;
 	}
 
 	/*
@@ -191,7 +217,9 @@ TransactionIdDidAbort(TransactionId transactionId)
 		return true;
 
 	/*
-	 * If it's marked subcommitted, we have to check the parent recursively.
+	 * If it's marked subcommitted, we have to check the topxid, see
+	 * TransactionIdDidCommit().
+	 *
 	 * However, if it's older than TransactionXmin, we can't look at
 	 * pg_subtrans; instead assume that the parent crashed without cleaning up
 	 * its children.
@@ -202,15 +230,31 @@ TransactionIdDidAbort(TransactionId transactionId)
 
 		if (TransactionIdPrecedes(transactionId, TransactionXmin))
 			return true;
-		parentXid = SubTransGetParent(transactionId);
-		if (!TransactionIdIsValid(parentXid))
+		parentXid = SubTransGetTopmostTransaction(transactionId);
+		if (parentXid == transactionId)
 		{
 			/* see notes in TransactionIdDidCommit */
 			elog(WARNING, "no pg_subtrans entry for subcommitted XID %u",
 				 transactionId);
 			return true;
 		}
-		return TransactionIdDidAbort(parentXid);
+
+		/*
+		 * Get the topxid status without delay, even though in the typical case
+		 * we are racing against the final commit.
+		 */
+		xidstatus = TransactionLogFetch(parentXid);
+
+		/*
+		 * Topxid should never show as subcommitted; see clog.c
+		 */
+		Assert(xidstatus != TRANSACTION_STATUS_SUB_COMMITTED);
+
+		/*
+		 * If it's marked aborted, it's aborted.
+		 */
+		if (xidstatus == TRANSACTION_STATUS_ABORTED)
+			return true;
 	}
 
 	/*
diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c
index 8086b857b9..be15ac25f3 100644
--- a/src/backend/access/transam/xact.c
+++ b/src/backend/access/transam/xact.c
@@ -696,8 +696,18 @@ AssignTransactionId(TransactionState s)
 		XactTopFullTransactionId = s->fullTransactionId;
 
 	if (isSubXact)
-		SubTransSetParent(XidFromFullTransactionId(s->fullTransactionId),
-						  XidFromFullTransactionId(s->parent->fullTransactionId));
+	{
+		TransactionId subxid = XidFromFullTransactionId(s->fullTransactionId);
+		TransactionId topxid = GetTopTransactionId();
+
+		/*
+		 * Insert entries into subtrans for this xid, noting that the entry
+		 * points directly to the topxid, not the immediate parent. This is
+		 * so it is faster in a long chain of subxids, because the
+		 * algorithm is then O(1), no matter how many subxids are assigned.
+		 */
+		SubTransSetParent(subxid, topxid);
+	}
 
 	/*
 	 * If it's a top-level transaction, the predicate locking system needs to
diff --git a/src/backend/storage/ipc/procarray.c b/src/backend/storage/ipc/procarray.c
index 9e8b6756fe..6cfb1a42c5 100644
--- a/src/backend/storage/ipc/procarray.c
+++ b/src/backend/storage/ipc/procarray.c
@@ -262,6 +262,14 @@ static ProcArrayStruct *procArray;
 
 static PGPROC *allProcs;
 
+/*
+ * Remember the last call to TransactionIdIsInProgress() so we can avoid calling
+ * SubTransGetTopMostTransaction() when the subxid is present in the procarray,
+ * so preventing data contention from becoming subtrans contention as well.
+ */
+static TransactionId LastCallXidIsInProgressSubXid = InvalidTransactionId;
+static TransactionId LastCallXidIsInProgressParentXid = InvalidTransactionId;
+
 /*
  * Cache to reduce overhead of repeated calls to TransactionIdIsInProgress()
  */
@@ -1310,14 +1318,14 @@ ProcArrayApplyXidAssignment(TransactionId topxid,
 
 	/*
 	 * Notice that we update pg_subtrans with the top-level xid, rather than
-	 * the parent xid. This is a difference between normal processing and
-	 * recovery, yet is still correct in all cases. The reason is that
+	 * the parent xid, which is correct in all cases. The reason is that
 	 * subtransaction commit is not marked in clog until commit processing, so
 	 * all aborted subtransactions have already been clearly marked in clog.
 	 * As a result we are able to refer directly to the top-level
 	 * transaction's state rather than skipping through all the intermediate
 	 * states in the subtransaction tree. This should be the first time we
-	 * have attempted to SubTransSetParent().
+	 * have attempted to SubTransSetParent() for this xid in recovery.
+	 * XXX this may be optimized later, but we don't have good test coverage.
 	 */
 	for (i = 0; i < nsubxids; i++)
 		SubTransSetParent(subxids[i], topxid);
@@ -1441,6 +1449,8 @@ TransactionIdIsInProgress(TransactionId xid)
 	other_xids = ProcGlobal->xids;
 	other_subxidstates = ProcGlobal->subxidStates;
 
+	LastCallXidIsInProgressSubXid = LastCallXidIsInProgressParentXid = InvalidTransactionId;
+
 	LWLockAcquire(ProcArrayLock, LW_SHARED);
 
 	/*
@@ -1509,6 +1519,15 @@ TransactionIdIsInProgress(TransactionId xid)
 			{
 				LWLockRelease(ProcArrayLock);
 				xc_by_child_xid_inc();
+
+				/*
+				 * Remember the parent xid, for use during XactLockTableWait().
+				 * We do this because it is cheaper than looking up pg_subtrans,
+				 * and also allows us to reduce calls to subtrans.
+				 */
+				LastCallXidIsInProgressSubXid = xid;
+				LastCallXidIsInProgressParentXid = pxid;
+
 				return true;
 			}
 		}
@@ -1589,12 +1608,38 @@ TransactionIdIsInProgress(TransactionId xid)
 	Assert(TransactionIdIsValid(topxid));
 	if (!TransactionIdEquals(topxid, xid) &&
 		pg_lfind32(topxid, xids, nxids))
+	{
+		LastCallXidIsInProgressSubXid = xid;
+		LastCallXidIsInProgressParentXid = topxid;
 		return true;
+	}
 
 	cachedXidIsNotInProgress = xid;
 	return false;
 }
 
+/*
+ * Allow the topmost xid to be accessed from the last call to
+ * TransactionIdIsInProgress(). Specifically designed for use in
+ * XactLockTableWait().
+ */
+bool
+GetTopmostTransactionIdFromProcArray(TransactionId xid, TransactionId *pxid)
+{
+	bool found = false;
+
+	Assert(TransactionIdIsNormal(xid));
+
+	if (LastCallXidIsInProgressSubXid == xid)
+	{
+		Assert(TransactionIdIsNormal(LastCallXidIsInProgressParentXid));
+		*pxid = LastCallXidIsInProgressParentXid;
+		found = true;
+	}
+
+	return found;
+}
+
 /*
  * TransactionIdIsActive -- is xid the top-level XID of an active backend?
  *
@@ -2352,9 +2397,8 @@ GetSnapshotData(Snapshot snapshot)
 			xip[count++] = xid;
 
 			/*
-			 * Save subtransaction XIDs if possible (if we've already
-			 * overflowed, there's no point).  Note that the subxact XIDs must
-			 * be later than their parent, so no need to check them against
+			 * Save subxid cache into snapshot. Note that the subxact XIDs
+			 * must be later than their parent, so no need to check them against
 			 * xmin.  We could filter against xmax, but it seems better not to
 			 * do that much work while holding the ProcArrayLock.
 			 *
@@ -2366,27 +2410,34 @@ GetSnapshotData(Snapshot snapshot)
 			 *
 			 * Again, our own XIDs are not included in the snapshot.
 			 */
-			if (!suboverflowed)
+			if (subxidStates[pgxactoff].overflowed)
+				suboverflowed = true;
+
+			/*
+			 * Include all subxids, whether or not we overflowed. This is
+			 * important because it can reduce the number of accesses to SUBTRANS
+			 * when we search snapshots, which is important for scalability,
+			 * especially in the presence of both overflowed and long running xacts.
+			 * This is true when fraction of subxids held in subxip is a large
+			 * fraction of the total subxids in use. In the case where one or more
+			 * transactions had more subxids in progress than the total size of
+			 * the cache this might not be true, but we consider that case to be
+			 * unlikely, even if it is possible.
+			 */
 			{
+				int			nsubxids = subxidStates[pgxactoff].count;
 
-				if (subxidStates[pgxactoff].overflowed)
-					suboverflowed = true;
-				else
+				if (nsubxids > 0)
 				{
-					int			nsubxids = subxidStates[pgxactoff].count;
-
-					if (nsubxids > 0)
-					{
-						int			pgprocno = pgprocnos[pgxactoff];
-						PGPROC	   *proc = &allProcs[pgprocno];
+					int			pgprocno = pgprocnos[pgxactoff];
+					PGPROC	   *proc = &allProcs[pgprocno];
 
-						pg_read_barrier();	/* pairs with GetNewTransactionId */
+					pg_read_barrier();	/* pairs with GetNewTransactionId */
 
-						memcpy(snapshot->subxip + subcount,
-							   (void *) proc->subxids.xids,
-							   nsubxids * sizeof(TransactionId));
-						subcount += nsubxids;
-					}
+					memcpy(snapshot->subxip + subcount,
+						   (void *) proc->subxids.xids,
+						   nsubxids * sizeof(TransactionId));
+					subcount += nsubxids;
 				}
 			}
 		}
@@ -2712,7 +2763,8 @@ ProcArrayInstallRestoredXmin(TransactionId xmin, PGPROC *proc)
  * that bookkeeping.
  *
  * Note that if any transaction has overflowed its cached subtransactions
- * then there is no real need include any subtransactions.
+ * then there is no real need include any subtransactions, which is
+ * different from GetSnapshotData which always stored all available subxids.
  */
 RunningTransactions
 GetRunningTransactionData(void)
diff --git a/src/backend/storage/lmgr/lmgr.c b/src/backend/storage/lmgr/lmgr.c
index 1043068bac..37b098d186 100644
--- a/src/backend/storage/lmgr/lmgr.c
+++ b/src/backend/storage/lmgr/lmgr.c
@@ -694,6 +694,8 @@ XactLockTableWait(TransactionId xid, Relation rel, ItemPointer ctid,
 
 	for (;;)
 	{
+		TransactionId pxid = InvalidTransactionId;
+
 		Assert(TransactionIdIsValid(xid));
 		Assert(!TransactionIdEquals(xid, GetTopTransactionIdIfAny()));
 
@@ -724,7 +726,27 @@ XactLockTableWait(TransactionId xid, Relation rel, ItemPointer ctid,
 		if (!first)
 			pg_usleep(1000L);
 		first = false;
-		xid = SubTransGetTopmostTransaction(xid);
+
+		/*
+		 * In most cases, we can get the parent xid from our prior call to
+		 * TransactionIdIsInProgress(), except in hot standby. If not, we have
+		 * to ask subtrans for the parent.
+		 */
+		if (GetTopmostTransactionIdFromProcArray(xid, &pxid))
+		{
+			/* TESTED-BY src/test/isolation/specs/subx-overflow.spec test3 */
+			Assert(TransactionIdIsValid(pxid));
+			xid = pxid;
+		}
+		else
+		{
+			/*
+			 * We can get here if RecoveryInProgress() during the last call to
+			 * TransactionIdIsInProgress(), but we don't Assert that here since
+			 * recovery might end before we check.
+			 */
+			xid = SubTransGetTopmostTransaction(xid);
+		}
 	}
 
 	if (oper != XLTW_None)
@@ -745,6 +767,8 @@ ConditionalXactLockTableWait(TransactionId xid)
 
 	for (;;)
 	{
+		TransactionId pxid = InvalidTransactionId;
+
 		Assert(TransactionIdIsValid(xid));
 		Assert(!TransactionIdEquals(xid, GetTopTransactionIdIfAny()));
 
@@ -762,7 +786,15 @@ ConditionalXactLockTableWait(TransactionId xid)
 		if (!first)
 			pg_usleep(1000L);
 		first = false;
-		xid = SubTransGetTopmostTransaction(xid);
+
+		/* See XactLockTableWait about this case */
+		if (GetTopmostTransactionIdFromProcArray(xid, &pxid))
+		{
+			Assert(TransactionIdIsValid(pxid));
+			xid = pxid;
+		}
+		else
+			xid = SubTransGetTopmostTransaction(xid);
 	}
 
 	return true;
diff --git a/src/backend/utils/time/snapmgr.c b/src/backend/utils/time/snapmgr.c
index 2524b1c585..9cd5065085 100644
--- a/src/backend/utils/time/snapmgr.c
+++ b/src/backend/utils/time/snapmgr.c
@@ -639,13 +639,9 @@ CopySnapshot(Snapshot snapshot)
 		newsnap->xip = NULL;
 
 	/*
-	 * Setup subXID array. Don't bother to copy it if it had overflowed,
-	 * though, because it's not used anywhere in that case. Except if it's a
-	 * snapshot taken during recovery; all the top-level XIDs are in subxip as
-	 * well in that case, so we mustn't lose them.
+	 * Setup subXID array, copying all subxids, even if one has overflowed.
 	 */
-	if (snapshot->subxcnt > 0 &&
-		(!snapshot->suboverflowed || snapshot->takenDuringRecovery))
+	if (snapshot->subxcnt > 0)
 	{
 		newsnap->subxip = (TransactionId *) ((char *) newsnap + subxipoff);
 		memcpy(newsnap->subxip, snapshot->subxip,
@@ -1240,8 +1236,10 @@ ExportSnapshot(Snapshot snapshot)
 		snapshot->subxcnt + nchildren > GetMaxSnapshotSubxidCount())
 		appendStringInfoString(&buf, "sof:1\n");
 	else
-	{
 		appendStringInfoString(&buf, "sof:0\n");
+
+	/* then unconditionally, since we always include all subxids */
+	{
 		appendStringInfo(&buf, "sxcnt:%d\n", snapshot->subxcnt + nchildren);
 		for (i = 0; i < snapshot->subxcnt; i++)
 			appendStringInfo(&buf, "sxp:%u\n", snapshot->subxip[i]);
@@ -1492,7 +1490,7 @@ ImportSnapshot(const char *idstr)
 
 	snapshot.suboverflowed = parseIntFromText("sof:", &filebuf, path);
 
-	if (!snapshot.suboverflowed)
+	/* then unconditionally, since we always include all subxids */
 	{
 		snapshot.subxcnt = xcnt = parseIntFromText("sxcnt:", &filebuf, path);
 
@@ -1506,11 +1504,6 @@ ImportSnapshot(const char *idstr)
 		for (i = 0; i < xcnt; i++)
 			snapshot.subxip[i] = parseXidFromText("sxp:", &filebuf, path);
 	}
-	else
-	{
-		snapshot.subxcnt = 0;
-		snapshot.subxip = NULL;
-	}
 
 	snapshot.takenDuringRecovery = parseIntFromText("rec:", &filebuf, path);
 
@@ -2286,6 +2279,8 @@ RestoreTransactionSnapshot(Snapshot snapshot, void *source_pgproc)
 bool
 XidInMVCCSnapshot(TransactionId xid, Snapshot snapshot)
 {
+	bool		have_parent = false;
+
 	/*
 	 * Make a quick range check to eliminate most XIDs without looking at the
 	 * xip arrays.  Note that this is OK even if we convert a subxact XID to
@@ -2301,80 +2296,90 @@ XidInMVCCSnapshot(TransactionId xid, Snapshot snapshot)
 	if (TransactionIdFollowsOrEquals(xid, snapshot->xmax))
 		return true;
 
+	/*
+	 * Previously, if the snapshot overflowed we would check for missing
+	 * subxids by calling subtrans in all cases. This caused a sudden massive
+	 * performance hit following overflow of even a single backend.
+	 *
+	 * This new version reorders the operations in XidMVCCSnapshot, reducing
+	 * calls to SubTransGetTopmostTransaction to the absolute minimum needed
+	 * and smoothing out the performance drop following overflow.
+	 * The previous code was neat, but not efficient for the overflow case.
+	 *
+	 * First, we check whether the xid is in the snapshot cache. If not present
+	 * and the snapshot is not complete because we overflowed we then check
+	 * SubTransGetTopmostTransaction(). In the presence of just a few
+	 * overflowed xids this gives us a high probability of a cache hit,
+	 * which maintains the performance. If the number of overflowed xids
+	 * becomes very high then the cache is less useful and performance will
+	 * continue to drop as it did before, so the difference between the two
+	 * techniques is what occurs with smaller numbers of overflowed xids
+	 * where num of xids not in cache < num xids in cache, which is good
+	 * because that is by far the most common case.
+	 */
+retry_search:
+
 	/*
 	 * Snapshot information is stored slightly differently in snapshots taken
-	 * during recovery.
+	 * during recovery. xip is empty on standbys and all xids are in subxip.
 	 */
 	if (!snapshot->takenDuringRecovery)
 	{
+		if (pg_lfind32(xid, snapshot->xip, snapshot->xcnt))
+			return true;
+
 		/*
-		 * If the snapshot contains full subxact data, the fastest way to
-		 * check things is just to compare the given XID against both subxact
-		 * XIDs and top-level XIDs.  If the snapshot overflowed, we have to
-		 * use pg_subtrans to convert a subxact XID to its parent XID, but
-		 * then we need only look at top-level XIDs not subxacts.
+		 * If we have the parent xid, then the xid is not in snapshot
 		 */
-		if (!snapshot->suboverflowed)
-		{
-			/* we have full data, so search subxip */
-			if (pg_lfind32(xid, snapshot->subxip, snapshot->subxcnt))
-				return true;
-
-			/* not there, fall through to search xip[] */
-		}
-		else
-		{
-			/*
-			 * Snapshot overflowed, so convert xid to top-level.  This is safe
-			 * because we eliminated too-old XIDs above.
-			 */
-			xid = SubTransGetTopmostTransaction(xid);
+		if (have_parent)
+			return false;
+	}
 
-			/*
-			 * If xid was indeed a subxact, we might now have an xid < xmin,
-			 * so recheck to avoid an array scan.  No point in rechecking
-			 * xmax.
-			 */
-			if (TransactionIdPrecedes(xid, snapshot->xmin))
-				return false;
-		}
+	/*
+	 * Now search subxip, which contains first 64 subxids of each topxid.
+	 */
+	if (pg_lfind32(xid, snapshot->subxip, snapshot->subxcnt))
+		return true;
 
-		if (pg_lfind32(xid, snapshot->xip, snapshot->xcnt))
-			return true;
-	}
-	else
+	if (!have_parent && snapshot->suboverflowed)
 	{
+		TransactionId pxid;
+
+		/* TESTED-BY src/test/isolation/specs/subx-overflow.spec test1 and test2 */
+
 		/*
-		 * In recovery we store all xids in the subxip array because it is by
-		 * far the bigger array, and we mostly don't know which xids are
-		 * top-level and which are subxacts. The xip array is empty.
+		 * If we haven't found xid yet, it might be because it is a subxid
+		 * that is not present because we overflowed, but it might also be
+		 * because the xid is not in the snapshot.
 		 *
-		 * We start by searching subtrans, if we overflowed.
+		 * It is important we do this step last because it is expensive,
+		 * and if everybody does this then SubTransSLRU is heavily contended.
 		 */
-		if (snapshot->suboverflowed)
-		{
-			/*
-			 * Snapshot overflowed, so convert xid to top-level.  This is safe
-			 * because we eliminated too-old XIDs above.
-			 */
-			xid = SubTransGetTopmostTransaction(xid);
 
+		/*
+		 * Snapshot overflowed, so convert xid to top-level.  This is safe
+		 * because we eliminated too-old XIDs above. Every overflowed subxid
+		 * will always have a parent recorded during AssignTransactionId()
+		 * so this should always return a valid TransactionId, if a subxact.
+		 */
+		pxid = SubTransGetTopmostTransaction(xid);
+
+		/*
+		 * Check whether xid was a subxact. If we now have a parent xid, loop.
+		 */
+		if (TransactionIdPrecedes(pxid, xid))
+		{
 			/*
 			 * If xid was indeed a subxact, we might now have an xid < xmin,
-			 * so recheck to avoid an array scan.  No point in rechecking
-			 * xmax.
+			 * so recheck to avoid an array scan. No point in rechecking xmax.
 			 */
-			if (TransactionIdPrecedes(xid, snapshot->xmin))
+			if (TransactionIdPrecedes(pxid, snapshot->xmin))
 				return false;
-		}
 
-		/*
-		 * We now have either a top-level xid higher than xmin or an
-		 * indeterminate xid. We don't know whether it's top level or subxact
-		 * but it doesn't matter. If it's present, the xid is visible.
-		 */
-		if (pg_lfind32(xid, snapshot->subxip, snapshot->subxcnt))
-			return true;
+			xid = pxid;
+			have_parent = true;
+			goto retry_search; /* search arrays again, now we have parent */
+		}
 	}
 
 	return false;
diff --git a/src/include/access/subtrans.h b/src/include/access/subtrans.h
index f94e116640..50f1b06197 100644
--- a/src/include/access/subtrans.h
+++ b/src/include/access/subtrans.h
@@ -15,7 +15,6 @@
 #define NUM_SUBTRANS_BUFFERS	32
 
 extern void SubTransSetParent(TransactionId xid, TransactionId parent);
-extern TransactionId SubTransGetParent(TransactionId xid);
 extern TransactionId SubTransGetTopmostTransaction(TransactionId xid);
 
 extern Size SUBTRANSShmemSize(void);
diff --git a/src/include/storage/procarray.h b/src/include/storage/procarray.h
index 9761f5374c..8f344dab6f 100644
--- a/src/include/storage/procarray.h
+++ b/src/include/storage/procarray.h
@@ -52,6 +52,8 @@ extern bool ProcArrayInstallRestoredXmin(TransactionId xmin, PGPROC *proc);
 extern RunningTransactions GetRunningTransactionData(void);
 
 extern bool TransactionIdIsInProgress(TransactionId xid);
+extern bool GetTopmostTransactionIdFromProcArray(TransactionId xid, TransactionId *pxid);
+
 extern bool TransactionIdIsActive(TransactionId xid);
 extern TransactionId GetOldestNonRemovableTransactionId(Relation rel);
 extern TransactionId GetOldestTransactionIdConsideredRunning(void);
