Use a hash table for catcache.c's CatCList objects.
authorTom Lane <[email protected]>
Fri, 22 Mar 2024 21:13:53 +0000 (17:13 -0400)
committerTom Lane <[email protected]>
Fri, 22 Mar 2024 21:13:53 +0000 (17:13 -0400)
Up to now, all of the "catcache list" objects within a catalog cache
were just chained together on a single dlist, requiring O(N) time to
search.  Remarkably, we've not had serious performance problems with
that so far; but we got a complaint of a bad performance regression
from v15 in a case with a large number of roles in the system, which
traced down to O(N^2) total time when we probed N catcache lists.

Replace that data structure with a hashtable having an enlargeable
number of dlists, in an exactly parallel way to the data structure
we've used for years for the plain CatCTup cache members.  The extra
cost of maintaining a hash table seems negligible, since we were
already computing a hash value for list searches.

Normally this'd be HEAD-only material, but in view of the performance
regression it seems advisable to back-patch into v16.  In the v16
version of the patch, leave the dead cc_lists field where it is and
add the new fields at the end of struct catcache, to avoid possible
ABI breakage in case any external code is looking at these structs.
(We assume no external code is actually allocating new catcache
structs.)

Per report from alex work.

Discussion: https://postgr.es/m/CAGvXd3OSMbJQwOSc-Tq-Ro1CAz=vggErdSG7pv2s6vmmTOLJSg@mail.gmail.com

src/backend/utils/cache/catcache.c
src/include/utils/catcache.h

index e85e56426130f1c3ba19c8127ddbac0c869a2fda..a2ebb50e0d214666bf28cacc81118cb0bdf44aee 100644 (file)
@@ -89,6 +89,8 @@ static void CatCachePrintStats(int code, Datum arg);
 #endif
 static void CatCacheRemoveCTup(CatCache *cache, CatCTup *ct);
 static void CatCacheRemoveCList(CatCache *cache, CatCList *cl);
+static void RehashCatCache(CatCache *cp);
+static void RehashCatCacheLists(CatCache *cp);
 static void CatalogCacheInitializeCache(CatCache *cache);
 static CatCTup *CatalogCacheCreateEntry(CatCache *cache,
                                        HeapTuple ntp, SysScanDesc scandesc,
@@ -393,6 +395,7 @@ CatCachePrintStats(int code, Datum arg)
    long        cc_neg_hits = 0;
    long        cc_newloads = 0;
    long        cc_invals = 0;
+   long        cc_nlists = 0;
    long        cc_lsearches = 0;
    long        cc_lhits = 0;
 
@@ -402,7 +405,7 @@ CatCachePrintStats(int code, Datum arg)
 
        if (cache->cc_ntup == 0 && cache->cc_searches == 0)
            continue;           /* don't print unused caches */
-       elog(DEBUG2, "catcache %s/%u: %d tup, %ld srch, %ld+%ld=%ld hits, %ld+%ld=%ld loads, %ld invals, %ld lsrch, %ld lhits",
+       elog(DEBUG2, "catcache %s/%u: %d tup, %ld srch, %ld+%ld=%ld hits, %ld+%ld=%ld loads, %ld invals, %d lists, %ld lsrch, %ld lhits",
             cache->cc_relname,
             cache->cc_indexoid,
             cache->cc_ntup,
@@ -414,6 +417,7 @@ CatCachePrintStats(int code, Datum arg)
             cache->cc_searches - cache->cc_hits - cache->cc_neg_hits - cache->cc_newloads,
             cache->cc_searches - cache->cc_hits - cache->cc_neg_hits,
             cache->cc_invals,
+            cache->cc_nlist,
             cache->cc_lsearches,
             cache->cc_lhits);
        cc_searches += cache->cc_searches;
@@ -421,10 +425,11 @@ CatCachePrintStats(int code, Datum arg)
        cc_neg_hits += cache->cc_neg_hits;
        cc_newloads += cache->cc_newloads;
        cc_invals += cache->cc_invals;
+       cc_nlists += cache->cc_nlist;
        cc_lsearches += cache->cc_lsearches;
        cc_lhits += cache->cc_lhits;
    }
-   elog(DEBUG2, "catcache totals: %d tup, %ld srch, %ld+%ld=%ld hits, %ld+%ld=%ld loads, %ld invals, %ld lsrch, %ld lhits",
+   elog(DEBUG2, "catcache totals: %d tup, %ld srch, %ld+%ld=%ld hits, %ld+%ld=%ld loads, %ld invals, %ld lists, %ld lsrch, %ld lhits",
         CacheHdr->ch_ntup,
         cc_searches,
         cc_hits,
@@ -434,6 +439,7 @@ CatCachePrintStats(int code, Datum arg)
         cc_searches - cc_hits - cc_neg_hits - cc_newloads,
         cc_searches - cc_hits - cc_neg_hits,
         cc_invals,
+        cc_nlists,
         cc_lsearches,
         cc_lhits);
 }
@@ -522,6 +528,8 @@ CatCacheRemoveCList(CatCache *cache, CatCList *cl)
                     cache->cc_keyno, cl->keys);
 
    pfree(cl);
+
+   --cache->cc_nlist;
 }
 
 
@@ -560,14 +568,19 @@ CatCacheInvalidate(CatCache *cache, uint32 hashValue)
     * Invalidate *all* CatCLists in this cache; it's too hard to tell which
     * searches might still be correct, so just zap 'em all.
     */
-   dlist_foreach_modify(iter, &cache->cc_lists)
+   for (int i = 0; i < cache->cc_nlbuckets; i++)
    {
-       CatCList   *cl = dlist_container(CatCList, cache_elem, iter.cur);
+       dlist_head *bucket = &cache->cc_lbucket[i];
 
-       if (cl->refcount > 0)
-           cl->dead = true;
-       else
-           CatCacheRemoveCList(cache, cl);
+       dlist_foreach_modify(iter, bucket)
+       {
+           CatCList   *cl = dlist_container(CatCList, cache_elem, iter.cur);
+
+           if (cl->refcount > 0)
+               cl->dead = true;
+           else
+               CatCacheRemoveCList(cache, cl);
+       }
    }
 
    /*
@@ -640,14 +653,19 @@ ResetCatalogCache(CatCache *cache)
    int         i;
 
    /* Remove each list in this cache, or at least mark it dead */
-   dlist_foreach_modify(iter, &cache->cc_lists)
+   for (i = 0; i < cache->cc_nlbuckets; i++)
    {
-       CatCList   *cl = dlist_container(CatCList, cache_elem, iter.cur);
+       dlist_head *bucket = &cache->cc_lbucket[i];
 
-       if (cl->refcount > 0)
-           cl->dead = true;
-       else
-           CatCacheRemoveCList(cache, cl);
+       dlist_foreach_modify(iter, bucket)
+       {
+           CatCList   *cl = dlist_container(CatCList, cache_elem, iter.cur);
+
+           if (cl->refcount > 0)
+               cl->dead = true;
+           else
+               CatCacheRemoveCList(cache, cl);
+       }
    }
 
    /* Remove each tuple in this cache, or at least mark it dead */
@@ -811,6 +829,12 @@ InitCatCache(int id,
                                     MCXT_ALLOC_ZERO);
    cp->cc_bucket = palloc0(nbuckets * sizeof(dlist_head));
 
+   /*
+    * Many catcaches never receive any list searches.  Therefore, we don't
+    * allocate the cc_lbuckets till we get a list search.
+    */
+   cp->cc_lbucket = NULL;
+
    /*
     * initialize the cache's relation information for the relation
     * corresponding to this cache, and initialize some of the new cache's
@@ -823,7 +847,9 @@ InitCatCache(int id,
    cp->cc_relisshared = false; /* temporary */
    cp->cc_tupdesc = (TupleDesc) NULL;
    cp->cc_ntup = 0;
+   cp->cc_nlist = 0;
    cp->cc_nbuckets = nbuckets;
+   cp->cc_nlbuckets = 0;
    cp->cc_nkeys = nkeys;
    for (i = 0; i < nkeys; ++i)
        cp->cc_keyno[i] = key[i];
@@ -885,6 +911,44 @@ RehashCatCache(CatCache *cp)
    cp->cc_bucket = newbucket;
 }
 
+/*
+ * Enlarge a catcache's list storage, doubling the number of buckets.
+ */
+static void
+RehashCatCacheLists(CatCache *cp)
+{
+   dlist_head *newbucket;
+   int         newnbuckets;
+   int         i;
+
+   elog(DEBUG1, "rehashing catalog cache id %d for %s; %d lists, %d buckets",
+        cp->id, cp->cc_relname, cp->cc_nlist, cp->cc_nlbuckets);
+
+   /* Allocate a new, larger, hash table. */
+   newnbuckets = cp->cc_nlbuckets * 2;
+   newbucket = (dlist_head *) MemoryContextAllocZero(CacheMemoryContext, newnbuckets * sizeof(dlist_head));
+
+   /* Move all entries from old hash table to new. */
+   for (i = 0; i < cp->cc_nlbuckets; i++)
+   {
+       dlist_mutable_iter iter;
+
+       dlist_foreach_modify(iter, &cp->cc_lbucket[i])
+       {
+           CatCList   *cl = dlist_container(CatCList, cache_elem, iter.cur);
+           int         hashIndex = HASH_INDEX(cl->hash_value, newnbuckets);
+
+           dlist_delete(iter.cur);
+           dlist_push_head(&newbucket[hashIndex], &cl->cache_elem);
+       }
+   }
+
+   /* Switch to the new array. */
+   pfree(cp->cc_lbucket);
+   cp->cc_nlbuckets = newnbuckets;
+   cp->cc_lbucket = newbucket;
+}
+
 /*
  *     CatalogCacheInitializeCache
  *
@@ -1527,7 +1591,9 @@ SearchCatCacheList(CatCache *cache,
    Datum       v4 = 0;         /* dummy last-column value */
    Datum       arguments[CATCACHE_MAXKEYS];
    uint32      lHashValue;
+   Index       lHashIndex;
    dlist_iter  iter;
+   dlist_head *lbucket;
    CatCList   *cl;
    CatCTup    *ct;
    List       *volatile ctlist;
@@ -1541,7 +1607,7 @@ SearchCatCacheList(CatCache *cache,
    /*
     * one-time startup overhead for each cache
     */
-   if (cache->cc_tupdesc == NULL)
+   if (unlikely(cache->cc_tupdesc == NULL))
        CatalogCacheInitializeCache(cache);
 
    Assert(nkeys > 0 && nkeys < cache->cc_nkeys);
@@ -1557,11 +1623,36 @@ SearchCatCacheList(CatCache *cache,
    arguments[3] = v4;
 
    /*
-    * compute a hash value of the given keys for faster search.  We don't
-    * presently divide the CatCList items into buckets, but this still lets
-    * us skip non-matching items quickly most of the time.
+    * If we haven't previously done a list search in this cache, create the
+    * bucket header array; otherwise, consider whether it's time to enlarge
+    * it.
+    */
+   if (cache->cc_lbucket == NULL)
+   {
+       /* Arbitrary initial size --- must be a power of 2 */
+       int         nbuckets = 16;
+
+       cache->cc_lbucket = (dlist_head *)
+           MemoryContextAllocZero(CacheMemoryContext,
+                                  nbuckets * sizeof(dlist_head));
+       /* Don't set cc_nlbuckets if we get OOM allocating cc_lbucket */
+       cache->cc_nlbuckets = nbuckets;
+   }
+   else
+   {
+       /*
+        * If the hash table has become too full, enlarge the buckets array.
+        * Quite arbitrarily, we enlarge when fill factor > 2.
+        */
+       if (cache->cc_nlist > cache->cc_nlbuckets * 2)
+           RehashCatCacheLists(cache);
+   }
+
+   /*
+    * Find the hash bucket in which to look for the CatCList.
     */
    lHashValue = CatalogCacheComputeHashValue(cache, nkeys, v1, v2, v3, v4);
+   lHashIndex = HASH_INDEX(lHashValue, cache->cc_nlbuckets);
 
    /*
     * scan the items until we find a match or exhaust our list
@@ -1569,7 +1660,8 @@ SearchCatCacheList(CatCache *cache,
     * Note: it's okay to use dlist_foreach here, even though we modify the
     * dlist within the loop, because we don't continue the loop afterwards.
     */
-   dlist_foreach(iter, &cache->cc_lists)
+   lbucket = &cache->cc_lbucket[lHashIndex];
+   dlist_foreach(iter, lbucket)
    {
        cl = dlist_container(CatCList, cache_elem, iter.cur);
 
@@ -1589,13 +1681,13 @@ SearchCatCacheList(CatCache *cache,
            continue;
 
        /*
-        * We found a matching list.  Move the list to the front of the
-        * cache's list-of-lists, to speed subsequent searches.  (We do not
+        * We found a matching list.  Move the list to the front of the list
+        * for its hashbucket, so as to speed subsequent searches.  (We do not
         * move the members to the fronts of their hashbucket lists, however,
         * since there's no point in that unless they are searched for
         * individually.)
         */
-       dlist_move_head(&cache->cc_lists, &cl->cache_elem);
+       dlist_move_head(lbucket, &cl->cache_elem);
 
        /* Bump the list's refcount and return it */
        ResourceOwnerEnlargeCatCacheListRefs(CurrentResourceOwner);
@@ -1806,7 +1898,12 @@ SearchCatCacheList(CatCache *cache,
    }
    Assert(i == nmembers);
 
-   dlist_push_head(&cache->cc_lists, &cl->cache_elem);
+   /*
+    * Add the CatCList to the appropriate bucket, and count it.
+    */
+   dlist_push_head(lbucket, &cl->cache_elem);
+
+   cache->cc_nlist++;
 
    /* Finally, bump the list's refcount and return it */
    cl->refcount++;
index af0b3418736f48b00d8d20232692eda5df775145..a32d7222a99c56302b17d4126ff6aba994121d5d 100644 (file)
@@ -62,6 +62,11 @@ typedef struct catcache
    ScanKeyData cc_skey[CATCACHE_MAXKEYS];  /* precomputed key info for heap
                                             * scans */
 
+   /* These fields are placed here to avoid ABI breakage in v16 */
+   int         cc_nlist;       /* # of CatCLists currently in this cache */
+   int         cc_nlbuckets;   /* # of CatCList hash buckets in this cache */
+   dlist_head *cc_lbucket;     /* hash buckets for CatCLists */
+
    /*
     * Keep these at the end, so that compiling catcache.c with CATCACHE_STATS
     * doesn't break ABI for other modules