Skip to content

Commit 187eabd

Browse files
committed
plpgsql: implement tail-call optimization for PLpgSQL routines
This patch implements tail-call optimization for the nested routine execution that is used to handle PLpgSQL control flow. PLpgSQL sub-routines are always tail calls because they are built as "continuation" functions, so we can always use the optimization for PLpgSQL. Tail-call optimization is only possible if the plan is not distributed (although we may not currently distribute such plans anyway). The optimization is performed by setting a `deferredRoutineReceiver` field on the planner before planning and running a nested routine. This `deferredRoutineReceiver` allows a routine in tail-call position to send the information needed to evaluate itself to its parent, and then return NULL. Once the parent routine receives the result, it checks whether `deferredRoutineReceiver` received a deferred nested routine, and if so, evaluates it to obtain the actual result. Given a simple looping function like the following: ``` CREATE FUNCTION f(n INT) RETURNS INT AS $$ DECLARE i INT := 0; BEGIN LOOP IF i >= n THEN EXIT; END IF; i := i + 1; END LOOP; RETURN i; END $$ LANGUAGE PLpgSQL; ``` This optimization takes runtime on my machine for `n=100000` from >20m to ~2s. Informs cockroachdb#105254 Release note: None
1 parent 5acb999 commit 187eabd

File tree

9 files changed

+101
-8
lines changed

9 files changed

+101
-8
lines changed

pkg/sql/apply_join.go

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
"github.com/cockroachdb/cockroach/pkg/sql/catalog/colinfo"
1919
"github.com/cockroachdb/cockroach/pkg/sql/catalog/descpb"
2020
"github.com/cockroachdb/cockroach/pkg/sql/opt/exec"
21+
"github.com/cockroachdb/cockroach/pkg/sql/sem/eval"
2122
"github.com/cockroachdb/cockroach/pkg/sql/sem/tree"
2223
"github.com/cockroachdb/cockroach/pkg/sql/types"
2324
"github.com/cockroachdb/cockroach/pkg/util/errorutil/unimplemented"
@@ -254,7 +255,9 @@ func (a *applyJoinNode) runNextRightSideIteration(params runParams, leftRow tree
254255
}
255256
plan := p.(*planComponents)
256257
rowResultWriter := NewRowResultWriter(&a.run.rightRows)
257-
if err := runPlanInsidePlan(ctx, params, plan, rowResultWriter); err != nil {
258+
if err := runPlanInsidePlan(
259+
ctx, params, plan, rowResultWriter, nil, /* deferredRoutineSender */
260+
); err != nil {
258261
return err
259262
}
260263
a.run.rightRowsIterator = newRowContainerIterator(ctx, a.run.rightRows)
@@ -264,7 +267,11 @@ func (a *applyJoinNode) runNextRightSideIteration(params runParams, leftRow tree
264267
// runPlanInsidePlan is used to run a plan and gather the results in the
265268
// resultWriter, as part of the execution of an "outer" plan.
266269
func runPlanInsidePlan(
267-
ctx context.Context, params runParams, plan *planComponents, resultWriter rowResultWriter,
270+
ctx context.Context,
271+
params runParams,
272+
plan *planComponents,
273+
resultWriter rowResultWriter,
274+
deferredRoutineSender eval.DeferredRoutineSender,
268275
) error {
269276
defer plan.close(ctx)
270277
execCfg := params.ExecCfg()
@@ -285,9 +292,13 @@ func runPlanInsidePlan(
285292
// we make sure to unset pausablePortal field on the planner.
286293
plannerCopy.pausablePortal = nil
287294
evalCtxFactory := func() *extendedEvalContext {
288-
evalCtx := params.p.ExtendedEvalContextCopy()
295+
plannerCopy.extendedEvalCtx = *params.p.ExtendedEvalContextCopy()
296+
evalCtx := &plannerCopy.extendedEvalCtx
289297
evalCtx.Planner = &plannerCopy
290298
evalCtx.StreamManagerFactory = &plannerCopy
299+
if deferredRoutineSender != nil {
300+
evalCtx.RoutineSender = deferredRoutineSender
301+
}
291302
return evalCtx
292303
}
293304

pkg/sql/opt/exec/execbuilder/scalar.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -700,6 +700,7 @@ func (b *Builder) buildExistsSubquery(
700700
true, /* calledOnNullInput */
701701
false, /* multiColOutput */
702702
false, /* generator */
703+
false, /* tailCall */
703704
),
704705
tree.DBoolFalse,
705706
}, types.Bool), nil
@@ -815,6 +816,7 @@ func (b *Builder) buildSubquery(
815816
true, /* calledOnNullInput */
816817
false, /* multiColOutput */
817818
false, /* generator */
819+
false, /* tailCall */
818820
), nil
819821
}
820822

@@ -869,6 +871,7 @@ func (b *Builder) buildSubquery(
869871
true, /* calledOnNullInput */
870872
false, /* multiColOutput */
871873
false, /* generator */
874+
false, /* tailCall */
872875
), nil
873876
}
874877

@@ -964,6 +967,7 @@ func (b *Builder) buildUDF(ctx *buildScalarCtx, scalar opt.ScalarExpr) (tree.Typ
964967
udf.Def.CalledOnNullInput,
965968
udf.Def.MultiColDataSource,
966969
udf.Def.SetReturning,
970+
udf.TailCall,
967971
), nil
968972
}
969973

pkg/sql/opt/ops/scalar.opt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1245,6 +1245,11 @@ define UDFCall {
12451245
define UDFCallPrivate {
12461246
# Def points to the UDF SQL body.
12471247
Def UDFDefinition
1248+
1249+
# TailCall indicates whether the UDF is in tail-call position, meaning that
1250+
# it is nested in a parent routine which will not perform any additional
1251+
# processing once this call is evaluated.
1252+
TailCall bool
12481253
}
12491254

12501255
# KVOptions is a set of KVOptionItems that specify arbitrary keys and values

pkg/sql/opt/optbuilder/plpgsql.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -470,7 +470,8 @@ func (b *plpgsqlBuilder) callContinuation(con *continuation, s *scope) *scope {
470470
for _, param := range b.params {
471471
addArg(tree.Name(param.Name), param.Typ)
472472
}
473-
call := b.ob.factory.ConstructUDFCall(args, &memo.UDFCallPrivate{Def: con.def})
473+
// PLpgSQL continuation routines are always in tail-call position.
474+
call := b.ob.factory.ConstructUDFCall(args, &memo.UDFCallPrivate{Def: con.def, TailCall: true})
474475

475476
returnColName := scopeColName("").WithMetadataName(con.def.Name)
476477
returnScope := s.push()

pkg/sql/recursive_cte.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,9 @@ func (n *recursiveCTENode) Next(params runParams) (bool, error) {
148148
opName := "recursive-cte-iteration-" + strconv.Itoa(n.iterationCount)
149149
ctx, sp := tracing.ChildSpan(params.ctx, opName)
150150
defer sp.Finish()
151-
if err := runPlanInsidePlan(ctx, params, newPlan.(*planComponents), rowResultWriter(n)); err != nil {
151+
if err := runPlanInsidePlan(
152+
ctx, params, newPlan.(*planComponents), rowResultWriter(n), nil, /* deferredRoutineSender */
153+
); err != nil {
152154
return false, err
153155
}
154156

pkg/sql/routine.go

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
"github.com/cockroachdb/cockroach/pkg/sql/sem/eval"
1919
"github.com/cockroachdb/cockroach/pkg/sql/sem/tree"
2020
"github.com/cockroachdb/cockroach/pkg/sql/types"
21+
"github.com/cockroachdb/cockroach/pkg/util"
2122
"github.com/cockroachdb/cockroach/pkg/util/tracing"
2223
"github.com/cockroachdb/errors"
2324
)
@@ -44,6 +45,18 @@ func (p *planner) EvalRoutineExpr(
4445
return expr.CachedResult, nil
4546
}
4647

48+
if expr.TailCall && !expr.Generator && p.EvalContext().RoutineSender != nil {
49+
// This is a nested routine in tail-call position.
50+
if !p.curPlan.flags.IsDistributed() && tailCallOptimizationEnabled {
51+
// Tail-call optimizations are enabled. Send the information needed to
52+
// evaluate this routine to the parent routine, then return. It is safe to
53+
// return NULL here because the parent is guaranteed not to perform any
54+
// processing on the result of the child.
55+
p.EvalContext().RoutineSender.SendDeferredRoutine(expr, args)
56+
return tree.DNull, nil
57+
}
58+
}
59+
4760
var g routineGenerator
4861
g.init(p, expr, args)
4962
defer g.Close(ctx)
@@ -98,9 +111,16 @@ type routineGenerator struct {
98111
rch rowContainerHelper
99112
rci *rowContainerIterator
100113
currVals tree.Datums
114+
// deferredRoutine encapsulates the information needed to execute a nested
115+
// routine that has deferred its execution.
116+
deferredRoutine struct {
117+
expr *tree.RoutineExpr
118+
args tree.Datums
119+
}
101120
}
102121

103122
var _ eval.ValueGenerator = &routineGenerator{}
123+
var _ eval.DeferredRoutineSender = &routineGenerator{}
104124

105125
// init initializes a routineGenerator.
106126
func (g *routineGenerator) init(p *planner, expr *tree.RoutineExpr, args tree.Datums) {
@@ -117,11 +137,28 @@ func (g *routineGenerator) ResolvedType() *types.T {
117137
}
118138

119139
// Start is part of the ValueGenerator interface.
140+
func (g *routineGenerator) Start(ctx context.Context, txn *kv.Txn) (err error) {
141+
for {
142+
err = g.startInternal(ctx, txn)
143+
if err != nil || g.deferredRoutine.expr == nil {
144+
// No tail-call optimization.
145+
return err
146+
}
147+
// A nested routine in tail-call position deferred its execution until now.
148+
// Since it's in tail-call position, evaluating it will give the result of
149+
// this routine as well.
150+
p, expr, args := g.p, g.deferredRoutine.expr, g.deferredRoutine.args
151+
g.Close(ctx)
152+
g.init(p, expr, args)
153+
}
154+
}
155+
156+
// startInternal implements logic for a single execution of a routine.
120157
// TODO(mgartner): We can cache results for future invocations of the routine by
121158
// creating a new iterator over an existing row container helper if the routine
122159
// is cache-able (i.e., there are no arguments to the routine and stepping is
123160
// disabled).
124-
func (g *routineGenerator) Start(ctx context.Context, txn *kv.Txn) (err error) {
161+
func (g *routineGenerator) startInternal(ctx context.Context, txn *kv.Txn) (err error) {
125162
rt := g.expr.ResolvedType()
126163
var retTypes []*types.T
127164
if g.expr.MultiColOutput {
@@ -179,7 +216,7 @@ func (g *routineGenerator) Start(ctx context.Context, txn *kv.Txn) (err error) {
179216
}
180217

181218
// Run the plan.
182-
err = runPlanInsidePlan(ctx, g.p.RunParams(ctx), plan.(*planComponents), w)
219+
err = runPlanInsidePlan(ctx, g.p.RunParams(ctx), plan.(*planComponents), w, g)
183220
if err != nil {
184221
return err
185222
}
@@ -213,9 +250,19 @@ func (g *routineGenerator) Values() (tree.Datums, error) {
213250
func (g *routineGenerator) Close(ctx context.Context) {
214251
if g.rci != nil {
215252
g.rci.Close()
216-
g.rci = nil
217253
}
218254
g.rch.Close(ctx)
255+
*g = routineGenerator{}
256+
}
257+
258+
var tailCallOptimizationEnabled = util.ConstantWithMetamorphicTestBool(
259+
"tail-call-optimization-enabled",
260+
true,
261+
)
262+
263+
func (g *routineGenerator) SendDeferredRoutine(routine *tree.RoutineExpr, args tree.Datums) {
264+
g.deferredRoutine.expr = routine
265+
g.deferredRoutine.args = args
219266
}
220267

221268
// droppingResultWriter drops all rows that are added to it. It only tracks

pkg/sql/sem/eval/context.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,11 @@ type Context struct {
278278
// JobsProfiler is the interface for builtins to extract job specific
279279
// execution details that may have been aggregated during a job's lifetime.
280280
JobsProfiler JobsProfiler
281+
282+
// RoutineSender allows nested routines in tail-call position to defer their
283+
// execution until control returns to the parent routine. It is only valid
284+
// during local execution. It may be unset.
285+
RoutineSender DeferredRoutineSender
281286
}
282287

283288
// JobsProfiler is the interface used to fetch job specific execution details

pkg/sql/sem/eval/deps.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -501,6 +501,15 @@ type ClientNoticeSender interface {
501501
BufferClientNotice(ctx context.Context, notice pgnotice.Notice)
502502
}
503503

504+
// DeferredRoutineSender allows a nested routine to send the information needed
505+
// for its own evaluation to a parent routine. This is used to defer execution
506+
// for tail-call optimization. It can only be used during local execution.
507+
type DeferredRoutineSender interface {
508+
// SendDeferredRoutine sends a local nested routine and its arguments to its
509+
// parent routine.
510+
SendDeferredRoutine(expr *tree.RoutineExpr, args tree.Datums)
511+
}
512+
504513
// PrivilegedAccessor gives access to certain queries that would otherwise
505514
// require someone with RootUser access to query a given data source.
506515
// It is defined independently to prevent a circular dependency on sql, tree and sqlbase.

pkg/sql/sem/tree/routine.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,13 @@ type RoutineExpr struct {
111111

112112
// Generator is true if the function may output a set of rows.
113113
Generator bool
114+
115+
// TailCall is true if the routine is in a tail-call position in a parent
116+
// routine. This means that once execution reaches this routine, the parent
117+
// routine will return the result of evaluating this routine with no further
118+
// changes. For routines in a tail-call position we implement an optimization
119+
// to avoid nesting execution. This is necessary for performant PLpgSQL loops.
120+
TailCall bool
114121
}
115122

116123
// NewTypedRoutineExpr returns a new RoutineExpr that is well-typed.
@@ -123,6 +130,7 @@ func NewTypedRoutineExpr(
123130
calledOnNullInput bool,
124131
multiColOutput bool,
125132
generator bool,
133+
tailCall bool,
126134
) *RoutineExpr {
127135
return &RoutineExpr{
128136
Args: args,
@@ -133,6 +141,7 @@ func NewTypedRoutineExpr(
133141
CalledOnNullInput: calledOnNullInput,
134142
MultiColOutput: multiColOutput,
135143
Generator: generator,
144+
TailCall: tailCall,
136145
}
137146
}
138147

0 commit comments

Comments
 (0)