Re: Optimize planner memory consumption for huge arrays

Поиск
Список
Период
Сортировка
От Tom Lane
Тема Re: Optimize planner memory consumption for huge arrays
Дата
Msg-id 1367418.1708816059@sss.pgh.pa.us
обсуждение исходный текст
Ответ на Re: Optimize planner memory consumption for huge arrays  (Tom Lane <tgl@sss.pgh.pa.us>)
Ответы Re: Optimize planner memory consumption for huge arrays  (Tomas Vondra <tomas.vondra@enterprisedb.com>)
Список pgsql-hackers
I wrote:
> Tomas Vondra <tomas.vondra@enterprisedb.com> writes:
>> On 2/19/24 16:45, Tom Lane wrote:
>>> Tomas Vondra <tomas.vondra@enterprisedb.com> writes:
>>>> For example, I don't think we expect selectivity functions to allocate
>>>> long-lived objects, right? So maybe we could run them in a dedicated
>>>> memory context, and reset it aggressively (after each call).

>>> That could eliminate a whole lot of potential leaks.  I'm not sure 
>>> though how much it moves the needle in terms of overall planner
>>> memory consumption.

>> It was an ad hoc thought, inspired by the issue at hand. Maybe it would
>> be possible to find similar "boundaries" in other parts of the planner.

> Here's a quick and probably-incomplete implementation of that idea.
> I've not tried to study its effects on memory consumption, just made
> sure it passes check-world.

I spent a bit more time on this patch.  One thing I was concerned
about was whether it causes any noticeable slowdown, and it seems that
it does: testing with "pgbench -S" I observe perhaps 1% slowdown.
However, we don't necessarily need to reset the temp context after
every single usage.  I experimented with resetting it every tenth
time, and that got me from 1% slower than HEAD to 1% faster.  Of
course "every tenth time" is very ad hoc.  I wondered if we could
make it somehow conditional on how much memory had been consumed
in the temp context, but there doesn't seem to be any cheap way
to get that.  Applying something like MemoryContextMemConsumed
would surely be a loser.  I'm not sure if it'd be worth extending
the mcxt.c API to provide something like "MemoryContextResetIfBig",
with some internal rule that would be cheap to apply like "reset
if we have any non-keeper blocks".

I also looked into whether it really does reduce overall memory
consumption noticeably, by collecting stats about planner memory
consumption during the core regression tests.  The answer is that
it barely helps.  I see the average used space across all planner
invocations drop from 23344 bytes to 23220, and the worst-case
numbers hardly move at all.  So that's a little discouraging.
But of course the regression tests prefer not to deal in very
large/expensive test cases, so maybe it's not surprising that
I don't see much win in this test.

Anyway, 0001 attached is a cleaned-up patch with the every-tenth-
time rule, and 0002 (not meant for commit) is the quick and
dirty instrumentation patch I used for collecting usage stats.

Even though this seems of only edge-case value, I'd much prefer
to do this than the sort of ad-hoc patching initially proposed
in this thread.

            regards, tom lane

diff --git a/src/backend/optimizer/path/clausesel.c b/src/backend/optimizer/path/clausesel.c
index c949dc1866..4678c778b0 100644
--- a/src/backend/optimizer/path/clausesel.c
+++ b/src/backend/optimizer/path/clausesel.c
@@ -24,6 +24,7 @@
 #include "statistics/statistics.h"
 #include "utils/fmgroids.h"
 #include "utils/lsyscache.h"
+#include "utils/memutils.h"
 #include "utils/selfuncs.h"

 /*
@@ -693,6 +694,7 @@ clause_selectivity_ext(PlannerInfo *root,
     Selectivity s1 = 0.5;        /* default for any unhandled clause type */
     RestrictInfo *rinfo = NULL;
     bool        cacheable = false;
+    MemoryContext saved_cxt;

     if (clause == NULL)            /* can this still happen? */
         return s1;
@@ -756,6 +758,21 @@ clause_selectivity_ext(PlannerInfo *root,
             clause = (Node *) rinfo->clause;
     }

+    /*
+     * Run all the remaining work in the short-lived planner_tmp_cxt, which
+     * we'll reset at the bottom.  This allows selectivity-related code to not
+     * be too concerned about leaking memory.
+     */
+    saved_cxt = MemoryContextSwitchTo(root->glob->planner_tmp_cxt);
+
+    /*
+     * This function can be called recursively, in which case we'd better not
+     * reset planner_tmp_cxt until we exit the topmost level.  Use of
+     * planner_tmp_cxt_depth also makes it safe for other places to use and
+     * reset planner_tmp_cxt in the same fashion.
+     */
+    root->glob->planner_tmp_cxt_depth++;
+
     if (IsA(clause, Var))
     {
         Var           *var = (Var *) clause;
@@ -967,6 +984,20 @@ clause_selectivity_ext(PlannerInfo *root,
             rinfo->outer_selec = s1;
     }

+    /* Exit and optionally clean up the planner_tmp_cxt */
+    MemoryContextSwitchTo(saved_cxt);
+    root->glob->planner_tmp_cxt_usage++;
+    Assert(root->glob->planner_tmp_cxt_depth > 0);
+    if (--root->glob->planner_tmp_cxt_depth == 0)
+    {
+        /* It's safe to reset the tmp context, but do we want to? */
+        if (root->glob->planner_tmp_cxt_usage >= PLANNER_TMP_CXT_USAGE_RESET)
+        {
+            MemoryContextReset(root->glob->planner_tmp_cxt);
+            root->glob->planner_tmp_cxt_usage = 0;
+        }
+    }
+
 #ifdef SELECTIVITY_DEBUG
     elog(DEBUG4, "clause_selectivity: s1 %f", s1);
 #endif                            /* SELECTIVITY_DEBUG */
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index be4e182869..df6a1fd330 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -307,6 +307,12 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions,
     glob = makeNode(PlannerGlobal);

     glob->boundParams = boundParams;
+    glob->planner_cxt = CurrentMemoryContext;
+    glob->planner_tmp_cxt = AllocSetContextCreate(glob->planner_cxt,
+                                                  "Planner temp context",
+                                                  ALLOCSET_DEFAULT_SIZES);
+    glob->planner_tmp_cxt_depth = 0;
+    glob->planner_tmp_cxt_usage = 0;
     glob->subplans = NIL;
     glob->subroots = NIL;
     glob->rewindPlanIDs = NULL;
@@ -586,6 +592,9 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions,
             result->jitFlags |= PGJIT_DEFORM;
     }

+    /* Clean up.  We aren't very thorough here. */
+    MemoryContextDelete(glob->planner_tmp_cxt);
+
     if (glob->partition_directory != NULL)
         DestroyPartitionDirectory(glob->partition_directory);

@@ -642,7 +651,7 @@ subquery_planner(PlannerGlobal *glob, Query *parse,
     root->parent_root = parent_root;
     root->plan_params = NIL;
     root->outer_params = NULL;
-    root->planner_cxt = CurrentMemoryContext;
+    root->planner_cxt = glob->planner_cxt;
     root->init_plans = NIL;
     root->cte_plan_ids = NIL;
     root->multiexpr_params = NIL;
@@ -6509,11 +6518,18 @@ plan_cluster_use_sort(Oid tableOid, Oid indexOid)

     glob = makeNode(PlannerGlobal);

+    glob->planner_cxt = CurrentMemoryContext;
+    glob->planner_tmp_cxt = AllocSetContextCreate(glob->planner_cxt,
+                                                  "Planner temp context",
+                                                  ALLOCSET_DEFAULT_SIZES);
+    glob->planner_tmp_cxt_depth = 0;
+    glob->planner_tmp_cxt_usage = 0;
+
     root = makeNode(PlannerInfo);
     root->parse = query;
     root->glob = glob;
     root->query_level = 1;
-    root->planner_cxt = CurrentMemoryContext;
+    root->planner_cxt = glob->planner_cxt;
     root->wt_param_id = -1;
     root->join_domains = list_make1(makeNode(JoinDomain));

@@ -6552,7 +6568,10 @@ plan_cluster_use_sort(Oid tableOid, Oid indexOid)
      * trust the index contents but use seqscan-and-sort.
      */
     if (lc == NULL)                /* not in the list? */
+    {
+        MemoryContextDelete(glob->planner_tmp_cxt);
         return true;            /* use sort */
+    }

     /*
      * Rather than doing all the pushups that would be needed to use
@@ -6584,6 +6603,9 @@ plan_cluster_use_sort(Oid tableOid, Oid indexOid)
                                       ForwardScanDirection, false,
                                       NULL, 1.0, false);

+    /* We assume this won't free *indexScanPath */
+    MemoryContextDelete(glob->planner_tmp_cxt);
+
     return (seqScanAndSortPath.total_cost < indexScanPath->path.total_cost);
 }

@@ -6631,11 +6653,18 @@ plan_create_index_workers(Oid tableOid, Oid indexOid)

     glob = makeNode(PlannerGlobal);

+    glob->planner_cxt = CurrentMemoryContext;
+    glob->planner_tmp_cxt = AllocSetContextCreate(glob->planner_cxt,
+                                                  "Planner temp context",
+                                                  ALLOCSET_DEFAULT_SIZES);
+    glob->planner_tmp_cxt_depth = 0;
+    glob->planner_tmp_cxt_usage = 0;
+
     root = makeNode(PlannerInfo);
     root->parse = query;
     root->glob = glob;
     root->query_level = 1;
-    root->planner_cxt = CurrentMemoryContext;
+    root->planner_cxt = glob->planner_cxt;
     root->wt_param_id = -1;
     root->join_domains = list_make1(makeNode(JoinDomain));

@@ -6725,6 +6754,8 @@ plan_create_index_workers(Oid tableOid, Oid indexOid)
         parallel_workers--;

 done:
+    MemoryContextDelete(glob->planner_tmp_cxt);
+
     index_close(index, NoLock);
     table_close(heap, NoLock);

diff --git a/src/backend/optimizer/prep/prepjointree.c b/src/backend/optimizer/prep/prepjointree.c
index aa83dd3636..6a36110df2 100644
--- a/src/backend/optimizer/prep/prepjointree.c
+++ b/src/backend/optimizer/prep/prepjointree.c
@@ -987,7 +987,7 @@ pull_up_simple_subquery(PlannerInfo *root, Node *jtnode, RangeTblEntry *rte,
     subroot->parent_root = root->parent_root;
     subroot->plan_params = NIL;
     subroot->outer_params = NULL;
-    subroot->planner_cxt = CurrentMemoryContext;
+    subroot->planner_cxt = root->planner_cxt;
     subroot->init_plans = NIL;
     subroot->cte_plan_ids = NIL;
     subroot->multiexpr_params = NIL;
diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h
index 534692bee1..92f465c55e 100644
--- a/src/include/nodes/pathnodes.h
+++ b/src/include/nodes/pathnodes.h
@@ -101,6 +101,18 @@ typedef struct PlannerGlobal
     /* Param values provided to planner() */
     ParamListInfo boundParams pg_node_attr(read_write_ignore);

+    /* context holding PlannerGlobal */
+    MemoryContext planner_cxt pg_node_attr(read_write_ignore);
+
+    /* short-lived context for purposes such as calling selectivity functions */
+    MemoryContext planner_tmp_cxt pg_node_attr(read_write_ignore);
+    /* nesting depth of uses of planner_tmp_cxt; reset it only at level 0 */
+    int            planner_tmp_cxt_depth;
+    /* how many times we've used planner_tmp_cxt since last reset */
+    int            planner_tmp_cxt_usage;
+    /* how often we want to reset it */
+#define PLANNER_TMP_CXT_USAGE_RESET 10
+
     /* Plans for SubPlan nodes */
     List       *subplans;

@@ -468,7 +480,7 @@ struct PlannerInfo
     /* List of MinMaxAggInfos */
     List       *minmax_aggs;

-    /* context holding PlannerInfo */
+    /* context holding PlannerInfo (copied from PlannerGlobal) */
     MemoryContext planner_cxt pg_node_attr(read_write_ignore);

     /* # of pages in all non-dummy tables of query */
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index df6a1fd330..db1d0fd7de 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -297,6 +297,10 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions,
     Plan       *top_plan;
     ListCell   *lp,
                *lr;
+    MemoryContextCounters before_counters,
+                after_counters;
+
+    MemoryContextMemConsumed(CurrentMemoryContext, &before_counters);

     /*
      * Set up global state for this planner invocation.  This data is needed
@@ -598,6 +602,13 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions,
     if (glob->partition_directory != NULL)
         DestroyPartitionDirectory(glob->partition_directory);

+    MemoryContextMemConsumed(CurrentMemoryContext, &after_counters);
+    after_counters.freespace -= before_counters.freespace;
+    after_counters.totalspace -= before_counters.totalspace;
+    elog(LOG, "planning memory consumption: used=%lld allocated=%lld",
+         (long long) (after_counters.totalspace - after_counters.freespace),
+         (long long) (after_counters.totalspace));
+
     return result;
 }


В списке pgsql-hackers по дате отправления:

Предыдущее
От: Thomas Munro
Дата:
Сообщение: Re: Relation bulk write facility
Следующее
От: Jim Jones
Дата:
Сообщение: Re: Patch: Add parse_type Function