Currently, nodeMemoize.c builds the hashtable for the cache during executor startup. This is not what is done in hash joins. I think we should make the two behave the same way.
Per [1] and the corresponding discussion leading to that, making a possibly large allocation at executor startup can lead to excessively long EXPLAIN (not EXPLAIN ANALYZE) times. This can confuse users as we don't mention in EXPLAIN where the time is being spent. Although there's not yet any conclusion that Memoize is to blame, there's another report from someone confused about where this time is being spent in [2]. Working on the Memoize code, I originally created the hash table during executor startup to save on having to check we have a table each time the node is executed. However, the branch for this should be quite predictable and I doubt it'll add any overhead that we would notice. The patch to do this is attached. David [1] https://git.postgresql.org/gitweb/?p=postgresql.git;a=commit;h=1e731ed12aa [2] https://postgr.es/m/61e642df-5f48-4e4e-b4c3-58936f90d...@thefreecat.org
diff --git a/src/backend/executor/nodeMemoize.c b/src/backend/executor/nodeMemoize.c index 0722e47777..1075178340 100644 --- a/src/backend/executor/nodeMemoize.c +++ b/src/backend/executor/nodeMemoize.c @@ -278,11 +278,14 @@ MemoizeHash_equal(struct memoize_hash *tb, const MemoizeKey *key1, } /* - * Initialize the hash table to empty. + * Initialize the hash table to empty. The MemoizeState's hashtable field + * must point to NULL. */ static void build_hash_table(MemoizeState *mstate, uint32 size) { + Assert(mstate->hashtable == NULL); + /* Make a guess at a good size when we're not given a valid size. */ if (size == 0) size = 1024; @@ -400,9 +403,12 @@ remove_cache_entry(MemoizeState *mstate, MemoizeEntry *entry) static void cache_purge_all(MemoizeState *mstate) { - uint64 evictions = mstate->hashtable->members; + uint64 evictions = 0; PlanState *pstate = (PlanState *) mstate; + if (mstate->hashtable != NULL) + evictions = mstate->hashtable->members; + /* * Likely the most efficient way to remove all items is to just reset the * memory context for the cache and then rebuild a fresh hash table. This @@ -410,8 +416,8 @@ cache_purge_all(MemoizeState *mstate) */ MemoryContextReset(mstate->tableContext); - /* Make the hash table the same size as the original size */ - build_hash_table(mstate, ((Memoize *) pstate->plan)->est_entries); + /* NULLify so we recreate the table on the next call */ + mstate->hashtable = NULL; /* reset the LRU list */ dlist_init(&mstate->lru_list); @@ -707,6 +713,10 @@ ExecMemoize(PlanState *pstate) Assert(node->entry == NULL); + /* first call? we'll need a hash table. */ + if (unlikely(node->hashtable == NULL)) + build_hash_table(node, ((Memoize *) pstate->plan)->est_entries); + /* * We're only ever in this state for the first call of the * scan. Here we have a look to see if we've already seen the @@ -1051,8 +1061,11 @@ ExecInitMemoize(Memoize *node, EState *estate, int eflags) /* Zero the statistics counters */ memset(&mstate->stats, 0, sizeof(MemoizeInstrumentation)); - /* Allocate and set up the actual cache */ - build_hash_table(mstate, node->est_entries); + /* + * Because it may require a large allocation we delay building of the hash + * table until executor run. + */ + mstate->hashtable = NULL; return mstate; } @@ -1062,6 +1075,7 @@ ExecEndMemoize(MemoizeState *node) { #ifdef USE_ASSERT_CHECKING /* Validate the memory accounting code is correct in assert builds. */ + if (node->hashtable != NULL) { int count; uint64 mem = 0;