me> First off, I thought that I managed to eliminate the degradation me> observed on the previous versions, but significant degradation (1.1% me> slower) is still seen in on case.
While trying benchmarking with many patterns, I noticed that it slows down catcache search significantly to call CatCacheCleanupOldEntries() even if the function does almost nothing. Oddly enough the degradation gets larger if I removed the counter-updating code from SearchCatCacheInternal. It seems that RehashCatCache is called far frequently than I thought and CatCacheCleanupOldEntries was suffering the branch penalty. The degradation vanished by a likely() attached to the condition. On the contrary patched version is constantly slightly faster than master. For now, I measured the patch with three access patterns as the catcachebench was designed. master patched-off patched-on(300s) test 1 3898.18ms 3896.11ms (-0.1%) 3889.44ms (- 0.2%) test 2 8013.37ms 8098.51ms (+1.1%) 8640.63ms (+ 7.8%) test 3 6146.95ms 6147.91ms (+0.0%) 15466 ms (+152 %) master : This patch is not applied. patched-off: This patch is applied and catalog_cache_prune_min_age = -1 patched-on : This patch is applied and catalog_cache_prune_min_age = 0 test 1: Creates many negative entries in STATRELATTINH (expiration doesn't happen) test 2: Repeat fetch several negative entries for many times. test 3: test 1 with expiration happens. The result looks far better, but the test 2 still shows a small degradation... I'll continue investigating it.. regards. -- Kyotaro Horiguchi NTT Open Source Software Center
>From 9516267f0e2943cf955cbbfe5133c13c36288ee6 Mon Sep 17 00:00:00 2001 From: Kyotaro Horiguchi <horikyoga....@gmail.com> Date: Fri, 6 Nov 2020 17:27:18 +0900 Subject: [PATCH v4] CatCache expiration feature --- src/backend/access/transam/xact.c | 3 + src/backend/utils/cache/catcache.c | 125 +++++++++++++++++++++++++++++ src/backend/utils/misc/guc.c | 12 +++ src/include/utils/catcache.h | 20 +++++ 4 files changed, 160 insertions(+) diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index af6afcebb1..a246fcc4c0 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -1086,6 +1086,9 @@ static void AtStart_Cache(void) { AcceptInvalidationMessages(); + + if (xactStartTimestamp != 0) + SetCatCacheClock(xactStartTimestamp); } /* diff --git a/src/backend/utils/cache/catcache.c b/src/backend/utils/cache/catcache.c index 3613ae5f44..f63224bfd5 100644 --- a/src/backend/utils/cache/catcache.c +++ b/src/backend/utils/cache/catcache.c @@ -38,6 +38,7 @@ #include "utils/rel.h" #include "utils/resowner_private.h" #include "utils/syscache.h" +#include "utils/timestamp.h" /* #define CACHEDEBUG */ /* turns DEBUG elogs on */ @@ -60,9 +61,18 @@ #define CACHE_elog(...) #endif +/* + * GUC variable to define the minimum age of entries that will be considered + * to be evicted in seconds. -1 to disable the feature. + */ +int catalog_cache_prune_min_age = -1; + /* Cache management header --- pointer is NULL until created */ static CatCacheHeader *CacheHdr = NULL; +/* Clock for the last accessed time of a catcache entry. */ +TimestampTz catcacheclock = 0; + static inline HeapTuple SearchCatCacheInternal(CatCache *cache, int nkeys, Datum v1, Datum v2, @@ -74,6 +84,7 @@ static pg_noinline HeapTuple SearchCatCacheMiss(CatCache *cache, Index hashIndex, Datum v1, Datum v2, Datum v3, Datum v4); +static bool CatCacheCleanupOldEntries(CatCache *cp); static uint32 CatalogCacheComputeHashValue(CatCache *cache, int nkeys, Datum v1, Datum v2, Datum v3, Datum v4); @@ -99,6 +110,12 @@ static void CatCacheFreeKeys(TupleDesc tupdesc, int nkeys, int *attnos, static void CatCacheCopyKeys(TupleDesc tupdesc, int nkeys, int *attnos, Datum *srckeys, Datum *dstkeys); +/* GUC assign function */ +void +assign_catalog_cache_prune_min_age(int newval, void *extra) +{ + catalog_cache_prune_min_age = newval; +} /* * internal support functions @@ -863,6 +880,10 @@ RehashCatCache(CatCache *cp) int newnbuckets; int i; + /* try removing old entries before expanding hash */ + if (CatCacheCleanupOldEntries(cp)) + return; + elog(DEBUG1, "rehashing catalog cache id %d for %s; %d tups, %d buckets", cp->id, cp->cc_relname, cp->cc_ntup, cp->cc_nbuckets); @@ -1264,6 +1285,20 @@ SearchCatCacheInternal(CatCache *cache, */ dlist_move_head(bucket, &ct->cache_elem); + /* + * Prolong life of this entry. Since we want run as less instructions + * as possible and want the branch be stable for performance reasons, + * we don't give a strict cap on the counter. All numbers above 1 will + * be regarded as 2 in CatCacheCleanupOldEntries(). + */ + if (unlikely(catalog_cache_prune_min_age >= 0)) + { + ct->naccess++; + if (unlikely(ct->naccess == 0)) + ct->naccess = 2; + ct->lastaccess = catcacheclock; + } + /* * If it's a positive entry, bump its refcount and return it. If it's * negative, we can report failure to the caller. @@ -1425,6 +1460,94 @@ SearchCatCacheMiss(CatCache *cache, return &ct->tuple; } +/* + * CatCacheCleanupOldEntries - Remove infrequently-used entries + * + * Catcache entries happen to be left unused for a long time for several + * reasons. Remove such entries to prevent catcache from bloating. It is based + * on the similar algorithm with buffer eviction. Entries that are accessed + * several times in a certain period live longer than those that have had less + * access in the same duration. + */ +static bool +CatCacheCleanupOldEntries(CatCache *cp) +{ + int nremoved = 0; + int i; + long oldest_ts = catcacheclock; + long age; + int us; + + /* Return immediately if disabled */ + if (likely(catalog_cache_prune_min_age < 0)) + return false; + + /* Don't scan the hash when we know we don't have prunable entries */ + TimestampDifference(cp->cc_oldest_ts, catcacheclock, &age, &us); + if (age < catalog_cache_prune_min_age) + return false; + + /* Scan over the whole hash to find entries to remove */ + for (i = 0 ; i < cp->cc_nbuckets ; i++) + { + dlist_mutable_iter iter; + + dlist_foreach_modify(iter, &cp->cc_bucket[i]) + { + CatCTup *ct = dlist_container(CatCTup, cache_elem, iter.cur); + + /* Don't remove referenced entries */ + if (ct->refcount == 0 && + (ct->c_list == NULL || ct->c_list->refcount == 0)) + { + /* + * Calculate the duration from the time from the last access + * to the "current" time. catcacheclock is updated + * per-statement basis and additionaly udpated periodically + * during a long running query. + */ + TimestampDifference(ct->lastaccess, catcacheclock, &age, &us); + + if (age > catalog_cache_prune_min_age) + { + /* + * Entries that are not accessed after the last pruning + * are removed in that seconds, and their lives are + * prolonged according to how many times they are accessed + * up to three times of the duration. We don't try shrink + * buckets since pruning effectively caps catcache + * expansion in the long term. + */ + if (ct->naccess > 2) + ct->naccess = 1; + else if (ct->naccess > 0) + ct->naccess--; + else + { + CatCacheRemoveCTup(cp, ct); + nremoved++; + + /* don't update oldest_ts by removed entry */ + continue; + } + } + } + + /* update oldest timestamp if the entry remains alive */ + if (ct->lastaccess < oldest_ts) + oldest_ts = ct->lastaccess; + } + } + + cp->cc_oldest_ts = oldest_ts; + + if (nremoved > 0) + elog(DEBUG1, "pruning catalog cache id=%d for %s: removed %d / %d", + cp->id, cp->cc_relname, nremoved, cp->cc_ntup + nremoved); + + return nremoved > 0; +} + /* * ReleaseCatCache * @@ -1888,6 +2011,8 @@ CatalogCacheCreateEntry(CatCache *cache, HeapTuple ntp, Datum *arguments, ct->dead = false; ct->negative = negative; ct->hash_value = hashValue; + ct->naccess = 0; + ct->lastaccess = catcacheclock; dlist_push_head(&cache->cc_bucket[hashIndex], &ct->cache_elem); diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c index a62d64eaa4..ca897cab2e 100644 --- a/src/backend/utils/misc/guc.c +++ b/src/backend/utils/misc/guc.c @@ -88,6 +88,7 @@ #include "utils/acl.h" #include "utils/builtins.h" #include "utils/bytea.h" +#include "utils/catcache.h" #include "utils/float.h" #include "utils/guc_tables.h" #include "utils/memutils.h" @@ -3399,6 +3400,17 @@ static struct config_int ConfigureNamesInt[] = check_huge_page_size, NULL, NULL }, + { + {"catalog_cache_prune_min_age", PGC_USERSET, RESOURCES_MEM, + gettext_noop("System catalog cache entries that are living unused more than this seconds are considered for removal."), + gettext_noop("The value of -1 turns off pruning."), + GUC_UNIT_S + }, + &catalog_cache_prune_min_age, + -1, -1, INT_MAX, + NULL, assign_catalog_cache_prune_min_age, NULL + }, + /* End-of-list marker */ { {NULL, 0, 0, NULL, NULL}, NULL, 0, 0, 0, NULL, NULL, NULL diff --git a/src/include/utils/catcache.h b/src/include/utils/catcache.h index f4aa316604..a11736f767 100644 --- a/src/include/utils/catcache.h +++ b/src/include/utils/catcache.h @@ -22,6 +22,7 @@ #include "access/htup.h" #include "access/skey.h" +#include "datatype/timestamp.h" #include "lib/ilist.h" #include "utils/relcache.h" @@ -61,6 +62,7 @@ typedef struct catcache slist_node cc_next; /* list link */ ScanKeyData cc_skey[CATCACHE_MAXKEYS]; /* precomputed key info for heap * scans */ + TimestampTz cc_oldest_ts; /* timestamp of the oldest tuple in the hash */ /* * Keep these at the end, so that compiling catcache.c with CATCACHE_STATS @@ -119,6 +121,8 @@ typedef struct catctup bool dead; /* dead but not yet removed? */ bool negative; /* negative cache entry? */ HeapTupleData tuple; /* tuple management header */ + unsigned int naccess; /* # of access to this entry */ + TimestampTz lastaccess; /* timestamp of the last usage */ /* * The tuple may also be a member of at most one CatCList. (If a single @@ -189,6 +193,22 @@ typedef struct catcacheheader /* this extern duplicates utils/memutils.h... */ extern PGDLLIMPORT MemoryContext CacheMemoryContext; + +/* for guc.c, not PGDLLPMPORT'ed */ +extern int catalog_cache_prune_min_age; + +/* source clock for access timestamp of catcache entries */ +extern TimestampTz catcacheclock; + +/* SetCatCacheClock - set catcache timestamp source clodk */ +static inline void +SetCatCacheClock(TimestampTz ts) +{ + catcacheclock = ts; +} + +extern void assign_catalog_cache_prune_min_age(int newval, void *extra); + extern void CreateCacheMemoryContext(void); extern CatCache *InitCatCache(int id, Oid reloid, Oid indexoid, -- 2.18.4