Joe Conway <m...@joeconway.com> writes:
> Not the most scientific of tests, but I think a reasonable one:
> ...
> 2.7% performance penalty

I made a slightly different test based on the original example.
This uses generate_series() which is surely faster than a SQL
function, so it shows the problem to a larger degree:

create table t1 as select x from generate_series(1,10000000) x;
vacuum t1;

-- time repeated executions of this:
select count(*) from t1
where exists (select 1 from generate_series(x,x+ (random()*10)::int));

-- and this:
select count(*) from t1
where exists (select 1 from
              generate_series(x,x+ (random()*10)::int::text::int));

The first of these test queries doesn't leak memory because the argument
expressions are all pass-by-value; the second one adds some useless text
conversions just to provoke the leak (it eats about 1GB in HEAD).

HEAD, with asserts off:

first query:
Time: 21694.557 ms
Time: 21629.107 ms
Time: 21593.510 ms

second query:
Time: 26698.445 ms
Time: 26699.408 ms
Time: 26751.742 ms

With yesterday's patch:

first query:
Time: 23919.438 ms
Time: 24053.108 ms
Time: 23884.989 ms
... so about 10.7% slower, not good

second query no longer bloats, but:
Time: 29986.850 ms
Time: 29951.179 ms
Time: 29771.533 ms
... almost 12% slower

With the attached patch instead, I get:

first query:
Time: 21017.503 ms
Time: 21113.656 ms
Time: 21107.617 ms
... 2.5% faster??

second query:
Time: 27033.626 ms
Time: 26997.907 ms
Time: 26984.397 ms
... about 1% slower

In the first query, the MemoryContextReset is nearly free since there's
nothing to free (and we've special-cased that path in aset.c).  It's
certainly a measurement artifact that it measures out faster, which says
to me that these numbers can only be trusted within a couple percent;
but at least we're not taking much hit in cases where the patch isn't
actually conferring any benefit.  For the second query, losing 1% to avoid
memory bloat seems well worthwhile.

Barring objections I'll apply and back-patch this.

                        regards, tom lane

diff --git a/src/backend/executor/execQual.c b/src/backend/executor/execQual.c
index f162e92..bc79e3a 100644
*** a/src/backend/executor/execQual.c
--- b/src/backend/executor/execQual.c
*************** ExecMakeFunctionResultNoSets(FuncExprSta
*** 2002,2007 ****
--- 2002,2008 ----
  Tuplestorestate *
  ExecMakeTableFunctionResult(ExprState *funcexpr,
  							ExprContext *econtext,
+ 							MemoryContext argContext,
  							TupleDesc expectedDesc,
  							bool randomAccess)
  {
*************** ExecMakeTableFunctionResult(ExprState *f
*** 2083,2094 ****
  		/*
  		 * Evaluate the function's argument list.
  		 *
! 		 * Note: ideally, we'd do this in the per-tuple context, but then the
! 		 * argument values would disappear when we reset the context in the
! 		 * inner loop.  So do it in caller context.  Perhaps we should make a
! 		 * separate context just to hold the evaluated arguments?
  		 */
  		argDone = ExecEvalFuncArgs(&fcinfo, fcache->args, econtext);
  		/* We don't allow sets in the arguments of the table function */
  		if (argDone != ExprSingleResult)
  			ereport(ERROR,
--- 2084,2101 ----
  		/*
  		 * Evaluate the function's argument list.
  		 *
! 		 * We can't do this in the per-tuple context: the argument values
! 		 * would disappear when we reset that context in the inner loop.  And
! 		 * the caller's CurrentMemoryContext is typically a query-lifespan
! 		 * context, so we don't want to leak memory there.  We require the
! 		 * caller to pass a separate memory context that can be used for this,
! 		 * and can be reset each time through to avoid bloat.
  		 */
+ 		MemoryContextReset(argContext);
+ 		oldcontext = MemoryContextSwitchTo(argContext);
  		argDone = ExecEvalFuncArgs(&fcinfo, fcache->args, econtext);
+ 		MemoryContextSwitchTo(oldcontext);
+ 
  		/* We don't allow sets in the arguments of the table function */
  		if (argDone != ExprSingleResult)
  			ereport(ERROR,
diff --git a/src/backend/executor/nodeFunctionscan.c b/src/backend/executor/nodeFunctionscan.c
index da5d8c1..945a414 100644
*** a/src/backend/executor/nodeFunctionscan.c
--- b/src/backend/executor/nodeFunctionscan.c
***************
*** 28,33 ****
--- 28,34 ----
  #include "nodes/nodeFuncs.h"
  #include "parser/parsetree.h"
  #include "utils/builtins.h"
+ #include "utils/memutils.h"
  
  
  /*
*************** FunctionNext(FunctionScanState *node)
*** 94,99 ****
--- 95,101 ----
  			node->funcstates[0].tstore = tstore =
  				ExecMakeTableFunctionResult(node->funcstates[0].funcexpr,
  											node->ss.ps.ps_ExprContext,
+ 											node->argcontext,
  											node->funcstates[0].tupdesc,
  										  node->eflags & EXEC_FLAG_BACKWARD);
  
*************** FunctionNext(FunctionScanState *node)
*** 152,157 ****
--- 154,160 ----
  			fs->tstore =
  				ExecMakeTableFunctionResult(fs->funcexpr,
  											node->ss.ps.ps_ExprContext,
+ 											node->argcontext,
  											fs->tupdesc,
  										  node->eflags & EXEC_FLAG_BACKWARD);
  
*************** ExecInitFunctionScan(FunctionScan *node,
*** 515,520 ****
--- 518,536 ----
  	ExecAssignResultTypeFromTL(&scanstate->ss.ps);
  	ExecAssignScanProjectionInfo(&scanstate->ss);
  
+ 	/*
+ 	 * Create a memory context that ExecMakeTableFunctionResult can use to
+ 	 * evaluate function arguments in.  We can't use the per-tuple context for
+ 	 * this because it gets reset too often; but we don't want to leak
+ 	 * evaluation results into the query-lifespan context either.  We just
+ 	 * need one context, because we evaluate each function separately.
+ 	 */
+ 	scanstate->argcontext = AllocSetContextCreate(CurrentMemoryContext,
+ 												  "Table function arguments",
+ 												  ALLOCSET_DEFAULT_MINSIZE,
+ 												  ALLOCSET_DEFAULT_INITSIZE,
+ 												  ALLOCSET_DEFAULT_MAXSIZE);
+ 
  	return scanstate;
  }
  
diff --git a/src/include/executor/executor.h b/src/include/executor/executor.h
index 5e4a15c..239aff3 100644
*** a/src/include/executor/executor.h
--- b/src/include/executor/executor.h
*************** extern Datum GetAttributeByName(HeapTupl
*** 231,236 ****
--- 231,237 ----
  				   bool *isNull);
  extern Tuplestorestate *ExecMakeTableFunctionResult(ExprState *funcexpr,
  							ExprContext *econtext,
+ 							MemoryContext argContext,
  							TupleDesc expectedDesc,
  							bool randomAccess);
  extern Datum ExecEvalExprSwitchContext(ExprState *expression, ExprContext *econtext,
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 1f7c6d1..b271f21 100644
*** a/src/include/nodes/execnodes.h
--- b/src/include/nodes/execnodes.h
*************** typedef struct SubqueryScanState
*** 1407,1412 ****
--- 1407,1413 ----
   *		nfuncs				number of functions being executed
   *		funcstates			per-function execution states (private in
   *							nodeFunctionscan.c)
+  *		argcontext			memory context to evaluate function arguments in
   * ----------------
   */
  struct FunctionScanPerFuncState;
*************** typedef struct FunctionScanState
*** 1421,1426 ****
--- 1422,1428 ----
  	int			nfuncs;
  	struct FunctionScanPerFuncState *funcstates;		/* array of length
  														 * nfuncs */
+ 	MemoryContext argcontext;
  } FunctionScanState;
  
  /* ----------------
-- 
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

Reply via email to