Hi,

On Sun, Feb 16, 2020 at 11:13 PM Pavel Stehule <pavel.steh...@gmail.com> wrote:
> when I do some profiling of plpgsql, usually I surprised how significant 
> overhead has expression execution. Any calculations are very slow.
>
> This is not typical example of plpgsql, but it shows cleanly where is a 
> overhead
>
> CREATE OR REPLACE FUNCTION public.foo()
>  RETURNS void
>  LANGUAGE plpgsql
>  IMMUTABLE
> AS $function$
> declare i bigint = 0;
> begin
>   while i < 100000000
>   loop
>     i := i + 1;
>   end loop;
> end;
> $function$
>
> Profile of development  version
>
>   10,04%  plpgsql.so                          [.] exec_eval_simple_expr
>    9,17%  postgres                            [.] AcquireExecutorLocks
>    7,01%  postgres                            [.] ExecInterpExpr
>    5,86%  postgres                            [.] 
> OverrideSearchPathMatchesCurrent
>    4,71%  postgres                            [.] GetCachedPlan
>    4,14%  postgres                            [.] AcquirePlannerLocks
>    3,72%  postgres                            [.] RevalidateCachedQuery
>    3,56%  postgres                            [.] MemoryContextReset
>    3,43%  plpgsql.so                          [.] plpgsql_param_eval_var

I was thinking about this overhead many months back and had even
written a patch to avoid going to the planner for "simple"
expressions, which can be handled by the executor.  Here is what the
performance looks like:

HEAD:

latency: 31979.393 ms

    18.32%  postgres  postgres           [.] ExecInterpExpr
    11.37%  postgres  plpgsql.so         [.] exec_eval_expr
     8.58%  postgres  plpgsql.so         [.] plpgsql_param_eval_var
     8.31%  postgres  plpgsql.so         [.] exec_stmt
     6.44%  postgres  postgres           [.] GetCachedPlan
     5.47%  postgres  postgres           [.] AcquireExecutorLocks
     5.30%  postgres  postgres           [.] RevalidateCachedQuery
     4.79%  postgres  plpgsql.so         [.] exec_assign_value
     4.41%  postgres  postgres           [.] SPI_plan_get_cached_plan
     4.36%  postgres  postgres           [.] MemoryContextReset
     4.22%  postgres  postgres           [.] ReleaseCachedPlan
     4.03%  postgres  postgres           [.] OverrideSearchPathMatchesCurrent
     2.63%  postgres  plpgsql.so         [.] exec_assign_expr
     2.11%  postgres  postgres           [.] int84lt
     1.95%  postgres  postgres           [.] ResourceOwnerForgetPlanCacheRef
     1.71%  postgres  postgres           [.] int84pl
     1.57%  postgres  postgres           [.] ResourceOwnerRememberPlanCacheRef
     1.38%  postgres  postgres           [.] recomputeNamespacePath
     1.35%  postgres  postgres           [.] ScanQueryForLocks
     1.24%  postgres  plpgsql.so         [.] exec_cast_value
     0.38%  postgres  postgres           [.] ResourceOwnerEnlargePlanCacheRefs
     0.05%  postgres  [kernel.kallsyms]  [k] __do_softirq
     0.03%  postgres  postgres           [.] GetUserId

Patched:

latency: 21011.871 ms

    28.26%  postgres  postgres           [.] ExecInterpExpr
    12.26%  postgres  plpgsql.so         [.] plpgsql_param_eval_var
    12.02%  postgres  plpgsql.so         [.] exec_stmt
    11.10%  postgres  plpgsql.so         [.] exec_eval_expr
    10.05%  postgres  postgres           [.] SPI_plan_is_valid
     7.09%  postgres  postgres           [.] MemoryContextReset
     6.65%  postgres  plpgsql.so         [.] exec_assign_value
     3.53%  postgres  plpgsql.so         [.] exec_assign_expr
     2.91%  postgres  postgres           [.] int84lt
     2.61%  postgres  postgres           [.] int84pl
     2.42%  postgres  plpgsql.so         [.] exec_cast_value
     0.86%  postgres  postgres           [.] CachedPlanIsValid
     0.16%  postgres  plpgsql.so         [.] SPI_plan_is_valid@plt
     0.05%  postgres  [kernel.kallsyms]  [k] __do_softirq
     0.03%  postgres  [kernel.kallsyms]  [k] finish_task_switch

I didn't send the patch, because it didn't handle the cases where a
simple expression consists of an inline-able function(s) in it, which
are better handled by a full-fledged planner call backed up by the
plan cache.  If we don't do that then every evaluation of such
"simple" expression needs to invoke the planner.  For example:

Consider this inline-able SQL function:

create or replace function sql_incr(a bigint)
returns int
immutable language sql as $$
select a+1;
$$;

Then this revised body of your function foo():

CREATE OR REPLACE FUNCTION public.foo()
 RETURNS int
 LANGUAGE plpgsql
 IMMUTABLE
AS $function$
declare i bigint = 0;
begin
  while i < 1000000
  loop
    i := sql_incr(i);
  end loop; return i;
end;
$function$
;

With HEAD `select foo()` finishes in 786 ms, whereas with the patch,
it takes 5102 ms.

I think the patch might be good idea to reduce the time to compute
simple expressions in plpgsql, if we can address the above issue.

Thanks,
Amit
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index b7c8d663fc..b292948853 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -16539,9 +16539,10 @@ CloneRowTriggersToPartition(Relation parent, Relation 
partition)
                 * Internal triggers require careful examination.  Ideally, we 
don't
                 * clone them.
                 *
-                * However, if our parent is a partitioned relation, there 
might be
-                * internal triggers that need cloning.  In that case, we must 
skip
-                * clone it if the trigger on parent depends on another trigger.
+                * However, if our parent is a partition itself, there might be
+                * internal triggers that need cloning.  For example, triggers 
on the
+                * parent that were in turn cloned from its own parent are 
marked
+                * internal, which too must be cloned to the partition.
                 *
                 * Note we dare not verify that the other trigger belongs to an
                 * ancestor relation of our parent, because that creates 
deadlock
diff --git a/src/pl/plpgsql/src/pl_exec.c b/src/pl/plpgsql/src/pl_exec.c
index 5acf604f63..72f156726f 100644
--- a/src/pl/plpgsql/src/pl_exec.c
+++ b/src/pl/plpgsql/src/pl_exec.c
@@ -319,8 +319,7 @@ static void exec_eval_cleanup(PLpgSQL_execstate *estate);
 static void exec_prepare_plan(PLpgSQL_execstate *estate,
                                                          PLpgSQL_expr *expr, 
int cursorOptions,
                                                          bool keepplan);
-static void exec_simple_check_plan(PLpgSQL_execstate *estate, PLpgSQL_expr 
*expr);
-static void exec_save_simple_expr(PLpgSQL_expr *expr, CachedPlan *cplan);
+static void exec_check_simple_expr(PLpgSQL_execstate *estate, PLpgSQL_expr 
*expr);
 static void exec_check_rw_parameter(PLpgSQL_expr *expr, int target_dno);
 static bool contains_target_param(Node *node, int *target_dno);
 static bool exec_eval_simple_expr(PLpgSQL_execstate *estate,
@@ -4062,7 +4061,7 @@ exec_prepare_plan(PLpgSQL_execstate *estate,
        expr->plan = plan;
 
        /* Check to see if it's a simple expression */
-       exec_simple_check_plan(estate, expr);
+       exec_check_simple_expr(estate, expr);
 
        /*
         * Mark expression as not using a read-write param.  exec_assign_value 
has
@@ -5758,9 +5757,10 @@ exec_eval_expr(PLpgSQL_execstate *estate,
        Form_pg_attribute attr;
 
        /*
-        * If first time through, create a plan for this expression.
+        * Create a plan for this expression, if first time through or the
+        * existing plan is no longer valid.
         */
-       if (expr->plan == NULL)
+       if (expr->plan == NULL || !SPI_plan_is_valid(expr->plan))
                exec_prepare_plan(estate, expr, CURSOR_OPT_PARALLEL_OK, true);
 
        /*
@@ -6076,7 +6076,6 @@ exec_eval_simple_expr(PLpgSQL_execstate *estate,
 {
        ExprContext *econtext = estate->eval_econtext;
        LocalTransactionId curlxid = MyProc->lxid;
-       CachedPlan *cplan;
        void       *save_setup_arg;
        bool            need_snapshot;
        MemoryContext oldcontext;
@@ -6093,30 +6092,15 @@ exec_eval_simple_expr(PLpgSQL_execstate *estate,
        if (expr->expr_simple_in_use && expr->expr_simple_lxid == curlxid)
                return false;
 
-       /*
-        * Revalidate cached plan, so that we will notice if it became stale. 
(We
-        * need to hold a refcount while using the plan, anyway.)  If replanning
-        * is needed, do that work in the eval_mcontext.
-        */
-       oldcontext = MemoryContextSwitchTo(get_eval_mcontext(estate));
-       cplan = SPI_plan_get_cached_plan(expr->plan);
-       MemoryContextSwitchTo(oldcontext);
+       /* Simple expressions aren't planned, so generation wouldn't change. */
+       Assert(expr->expr_simple_generation == 0);
 
        /*
-        * We can't get a failure here, because the number of CachedPlanSources 
in
-        * the SPI plan can't change from what exec_simple_check_plan saw; it's 
a
-        * property of the raw parsetree generated from the query text.
+        * better recheck r/w safety, as it could change due to inlining
+        * XXX - should no longer occur, because planning is not done?
         */
-       Assert(cplan != NULL);
-
-       /* If it got replanned, update our copy of the simple expression */
-       if (cplan->generation != expr->expr_simple_generation)
-       {
-               exec_save_simple_expr(expr, cplan);
-               /* better recheck r/w safety, as it could change due to 
inlining */
-               if (expr->rwparam >= 0)
-                       exec_check_rw_parameter(expr, expr->rwparam);
-       }
+       if (expr->rwparam >= 0)
+               exec_check_rw_parameter(expr, expr->rwparam);
 
        /*
         * Pass back previously-determined result type.
@@ -6192,11 +6176,6 @@ exec_eval_simple_expr(PLpgSQL_execstate *estate,
 
        MemoryContextSwitchTo(oldcontext);
 
-       /*
-        * Now we can release our refcount on the cached plan.
-        */
-       ReleaseCachedPlan(cplan, true);
-
        /*
         * That's it.
         */
@@ -7890,19 +7869,20 @@ get_cast_hashentry(PLpgSQL_execstate *estate,
 
 
 /* ----------
- * exec_simple_check_plan -            Check if a plan is simple enough to
+ * exec_check_simple_expr -            Check if the expression is simple 
enough to
  *                                                             be evaluated by 
ExecEvalExpr() instead
  *                                                             of SPI.
+ *
+ * If it is, set expr->expr_simple_expr.
  * ----------
  */
 static void
-exec_simple_check_plan(PLpgSQL_execstate *estate, PLpgSQL_expr *expr)
+exec_check_simple_expr(PLpgSQL_execstate *estate, PLpgSQL_expr *expr)
 {
        List       *plansources;
        CachedPlanSource *plansource;
        Query      *query;
-       CachedPlan *cplan;
-       MemoryContext oldcontext;
+       Expr       *tle_expr;
 
        /*
         * Initialize to "not simple".
@@ -7972,95 +7952,12 @@ exec_simple_check_plan(PLpgSQL_execstate *estate, 
PLpgSQL_expr *expr)
                return;
 
        /*
-        * OK, we can treat it as a simple plan.
-        *
-        * Get the generic plan for the query.  If replanning is needed, do that
-        * work in the eval_mcontext.
-        */
-       oldcontext = MemoryContextSwitchTo(get_eval_mcontext(estate));
-       cplan = SPI_plan_get_cached_plan(expr->plan);
-       MemoryContextSwitchTo(oldcontext);
-
-       /* Can't fail, because we checked for a single CachedPlanSource above */
-       Assert(cplan != NULL);
-
-       /* Share the remaining work with replan code path */
-       exec_save_simple_expr(expr, cplan);
-
-       /* Release our plan refcount */
-       ReleaseCachedPlan(cplan, true);
-}
-
-/*
- * exec_save_simple_expr --- extract simple expression from CachedPlan
- */
-static void
-exec_save_simple_expr(PLpgSQL_expr *expr, CachedPlan *cplan)
-{
-       PlannedStmt *stmt;
-       Plan       *plan;
-       Expr       *tle_expr;
-
-       /*
-        * Given the checks that exec_simple_check_plan did, none of the Asserts
-        * here should ever fail.
-        */
-
-       /* Extract the single PlannedStmt */
-       Assert(list_length(cplan->stmt_list) == 1);
-       stmt = linitial_node(PlannedStmt, cplan->stmt_list);
-       Assert(stmt->commandType == CMD_SELECT);
-
-       /*
-        * Ordinarily, the plan node should be a simple Result.  However, if
-        * force_parallel_mode is on, the planner might've stuck a Gather node
-        * atop that.  The simplest way to deal with this is to look through the
-        * Gather node.  The Gather node's tlist would normally contain a Var
-        * referencing the child node's output, but it could also be a Param, or
-        * it could be a Const that setrefs.c copied as-is.
-        */
-       plan = stmt->planTree;
-       for (;;)
-       {
-               /* Extract the single tlist expression */
-               Assert(list_length(plan->targetlist) == 1);
-               tle_expr = castNode(TargetEntry, 
linitial(plan->targetlist))->expr;
-
-               if (IsA(plan, Result))
-               {
-                       Assert(plan->lefttree == NULL &&
-                                  plan->righttree == NULL &&
-                                  plan->initPlan == NULL &&
-                                  plan->qual == NULL &&
-                                  ((Result *) plan)->resconstantqual == NULL);
-                       break;
-               }
-               else if (IsA(plan, Gather))
-               {
-                       Assert(plan->lefttree != NULL &&
-                                  plan->righttree == NULL &&
-                                  plan->initPlan == NULL &&
-                                  plan->qual == NULL);
-                       /* If setrefs.c copied up a Const, no need to look 
further */
-                       if (IsA(tle_expr, Const))
-                               break;
-                       /* Otherwise, it had better be a Param or an outer Var 
*/
-                       Assert(IsA(tle_expr, Param) ||(IsA(tle_expr, Var) &&
-                                                                               
   ((Var *) tle_expr)->varno == OUTER_VAR));
-                       /* Descend to the child node */
-                       plan = plan->lefttree;
-               }
-               else
-                       elog(ERROR, "unexpected plan node type: %d",
-                                (int) nodeTag(plan));
-       }
-
-       /*
-        * Save the simple expression, and initialize state to "not valid in
-        * current transaction".
+        * We have a simple expression.  Save it and initialize state to "not
+        * valid in current transaction".
         */
+       tle_expr = castNode(TargetEntry, linitial(query->targetList))->expr;
        expr->expr_simple_expr = tle_expr;
-       expr->expr_simple_generation = cplan->generation;
+       expr->expr_simple_generation = plansource->generation;
        expr->expr_simple_state = NULL;
        expr->expr_simple_in_use = false;
        expr->expr_simple_lxid = InvalidLocalTransactionId;
diff --git a/src/test/regress/expected/plpgsql.out 
b/src/test/regress/expected/plpgsql.out
index cd2c79f4d5..3abbc7bc57 100644
--- a/src/test/regress/expected/plpgsql.out
+++ b/src/test/regress/expected/plpgsql.out
@@ -4289,12 +4289,10 @@ end
 $$;
 select fail();
 ERROR:  division by zero
-CONTEXT:  SQL statement "SELECT 1/0"
-PL/pgSQL function fail() line 3 at RETURN
+CONTEXT:  PL/pgSQL function fail() line 3 at RETURN
 select fail();
 ERROR:  division by zero
-CONTEXT:  SQL statement "SELECT 1/0"
-PL/pgSQL function fail() line 3 at RETURN
+CONTEXT:  PL/pgSQL function fail() line 3 at RETURN
 drop function fail();
 -- Test handling of string literals.
 set standard_conforming_strings = off;

Reply via email to