diff --git a/src/backend/access/common/toast_dictionary.c b/src/backend/access/common/toast_dictionary.c index c4364a7fee..240c408e26 100644 --- a/src/backend/access/common/toast_dictionary.c +++ b/src/backend/access/common/toast_dictionary.c @@ -282,6 +282,57 @@ toast_dictionary_keep(Relation rel, AttrNumber attnum, const varlena *value) return result; } +/* + * The per-backend dictionary caches below hold DICT_CACHE_SIZE entries each, + * keyed by dictionary OID, least recently used out. More than one entry + * matters whenever reads alternate between dictionaries: two dictionary + * columns in one row, or one column retrained without a rewrite, whose old + * and new values sit side by side. With a single entry each such switch + * re-fetches the dictionary and, for zstd, digests it again. + */ +#if defined(USE_ZSTD) || defined(USE_LZ4) +#define DICT_CACHE_SIZE 4 + +static uint64 dict_cache_clock = 0; + +/* + * Return the slot of dictid in ids[], or -1. Marks a hit as just used. + */ +static int +dict_cache_lookup(Oid *ids, uint64 *used, Oid dictid) +{ + for (int i = 0; i < DICT_CACHE_SIZE; i++) + { + if (ids[i] == dictid) + { + used[i] = ++dict_cache_clock; + return i; + } + } + return -1; +} + +/* + * Pick the slot for a new entry: an empty one, else the least recently used. + * Call it only after dict_fetch(), which may run invalidation callbacks that + * empty the cache. + */ +static int +dict_cache_victim(Oid *ids, uint64 *used) +{ + int victim = 0; + + for (int i = 0; i < DICT_CACHE_SIZE; i++) + { + if (!OidIsValid(ids[i])) + return i; + if (used[i] < used[victim]) + victim = i; + } + return victim; +} +#endif + #ifdef USE_ZSTD /* * Per-backend cache of digested dictionaries. Digesting builds the entropy @@ -289,26 +340,30 @@ toast_dictionary_keep(Relation rel, AttrNumber attnum, const varlena *value) * ZSTD_CDict/ZSTD_DDict objects are kept and dropped on invalidation of * pg_toast_dictionary. * - * XXX: one entry per direction is enough for a scan of one column; a small - * hash table would serve workloads that mix many dictionaries. + * DICT_CACHE_SIZE entries per direction. */ -static Oid cdict_id = InvalidOid; -static ZSTD_CDict *cdict = NULL; -static Oid ddict_id = InvalidOid; -static ZSTD_DDict *ddict = NULL; +static Oid cdict_ids[DICT_CACHE_SIZE]; +static uint64 cdict_used[DICT_CACHE_SIZE]; +static ZSTD_CDict *cdicts[DICT_CACHE_SIZE]; +static Oid ddict_ids[DICT_CACHE_SIZE]; +static uint64 ddict_used[DICT_CACHE_SIZE]; +static ZSTD_DDict *ddicts[DICT_CACHE_SIZE]; static ZSTD_CCtx *dict_cctx = NULL; static ZSTD_DCtx *dict_dctx = NULL; static void dict_cache_invalidate(Datum arg, SysCacheIdentifier cacheid, uint32 hashvalue) { - if (cdict) - ZSTD_freeCDict(cdict); - if (ddict) - ZSTD_freeDDict(ddict); - cdict = NULL; - ddict = NULL; - cdict_id = ddict_id = InvalidOid; + for (int i = 0; i < DICT_CACHE_SIZE; i++) + { + if (cdicts[i]) + ZSTD_freeCDict(cdicts[i]); + if (ddicts[i]) + ZSTD_freeDDict(ddicts[i]); + cdicts[i] = NULL; + ddicts[i] = NULL; + cdict_ids[i] = ddict_ids[i] = InvalidOid; + } } #endif /* USE_ZSTD */ @@ -350,44 +405,52 @@ dict_fetch(Oid dictid) static ZSTD_CDict * dict_get_cdict(Oid dictid) { - if (cdict_id != dictid) + int slot = dict_cache_lookup(cdict_ids, cdict_used, dictid); + + if (slot < 0) { bytea *d = dict_fetch(dictid); - if (cdict) - ZSTD_freeCDict(cdict); - cdict_id = InvalidOid; - cdict = ZSTD_createCDict(VARDATA_ANY(d), VARSIZE_ANY_EXHDR(d), - ZSTD_CLEVEL_DEFAULT); + slot = dict_cache_victim(cdict_ids, cdict_used); + if (cdicts[slot]) + ZSTD_freeCDict(cdicts[slot]); + cdict_ids[slot] = InvalidOid; + cdicts[slot] = ZSTD_createCDict(VARDATA_ANY(d), VARSIZE_ANY_EXHDR(d), + ZSTD_CLEVEL_DEFAULT); pfree(d); - if (cdict == NULL) + if (cdicts[slot] == NULL) elog(ERROR, "could not load compression dictionary %u", dictid); - cdict_id = dictid; + cdict_ids[slot] = dictid; + cdict_used[slot] = ++dict_cache_clock; } if (dict_cctx == NULL && (dict_cctx = ZSTD_createCCtx()) == NULL) elog(ERROR, "could not create zstd compression context"); - return cdict; + return cdicts[slot]; } static ZSTD_DDict * dict_get_ddict(Oid dictid) { - if (ddict_id != dictid) + int slot = dict_cache_lookup(ddict_ids, ddict_used, dictid); + + if (slot < 0) { bytea *d = dict_fetch(dictid); - if (ddict) - ZSTD_freeDDict(ddict); - ddict_id = InvalidOid; - ddict = ZSTD_createDDict(VARDATA_ANY(d), VARSIZE_ANY_EXHDR(d)); + slot = dict_cache_victim(ddict_ids, ddict_used); + if (ddicts[slot]) + ZSTD_freeDDict(ddicts[slot]); + ddict_ids[slot] = InvalidOid; + ddicts[slot] = ZSTD_createDDict(VARDATA_ANY(d), VARSIZE_ANY_EXHDR(d)); pfree(d); - if (ddict == NULL) + if (ddicts[slot] == NULL) elog(ERROR, "could not load compression dictionary %u", dictid); - ddict_id = dictid; + ddict_ids[slot] = dictid; + ddict_used[slot] = ++dict_cache_clock; } if (dict_dctx == NULL && (dict_dctx = ZSTD_createDCtx()) == NULL) elog(ERROR, "could not create zstd decompression context"); - return ddict; + return ddicts[slot]; } #endif /* USE_ZSTD */ @@ -515,27 +578,32 @@ zstd_dict_compressed_size(const char *dict, size_t dictlen, #ifdef USE_LZ4 /* * Per-backend cache of the raw dictionary bytes most recently used, keyed by - * OID. Unlike zstd, lz4 has no digested dictionary object: LZ4_loadDict() + * OID, DICT_CACHE_SIZE entries. Unlike zstd, lz4 has no digested dictionary object: LZ4_loadDict() * and LZ4_decompress_safe_usingDict() take the dictionary bytes directly, so * caching the bytea is all that is needed. Dropped on invalidation of * pg_toast_dictionary. */ -static Oid lz4dict_id = InvalidOid; -static bytea *lz4dict = NULL; +static Oid lz4dict_ids[DICT_CACHE_SIZE]; +static uint64 lz4dict_used[DICT_CACHE_SIZE]; +static bytea *lz4dicts[DICT_CACHE_SIZE]; static void lz4_dict_cache_invalidate(Datum arg, SysCacheIdentifier cacheid, uint32 hashvalue) { - if (lz4dict) - pfree(lz4dict); - lz4dict = NULL; - lz4dict_id = InvalidOid; + for (int i = 0; i < DICT_CACHE_SIZE; i++) + { + if (lz4dicts[i]) + pfree(lz4dicts[i]); + lz4dicts[i] = NULL; + lz4dict_ids[i] = InvalidOid; + } } static bytea * lz4_dict_get(Oid dictid) { static bool registered = false; + int slot; if (!registered) { @@ -543,7 +611,8 @@ lz4_dict_get(Oid dictid) (Datum) 0); registered = true; } - if (lz4dict_id != dictid) + slot = dict_cache_lookup(lz4dict_ids, lz4dict_used, dictid); + if (slot < 0) { bytea *d = dict_fetch(dictid); bytea *cached; @@ -557,12 +626,14 @@ lz4_dict_get(Oid dictid) cached = (bytea *) MemoryContextAlloc(CacheMemoryContext, VARSIZE(d)); memcpy(cached, d, VARSIZE(d)); pfree(d); - if (lz4dict) - pfree(lz4dict); - lz4dict = cached; - lz4dict_id = dictid; + slot = dict_cache_victim(lz4dict_ids, lz4dict_used); + if (lz4dicts[slot]) + pfree(lz4dicts[slot]); + lz4dicts[slot] = cached; + lz4dict_ids[slot] = dictid; + lz4dict_used[slot] = ++dict_cache_clock; } - return lz4dict; + return lz4dicts[slot]; } #endif /* USE_LZ4 */