From: Andres Freund <andres@anarazel.de>
To: Andy Fan <zhihuifan1213@163.com>
Cc: Tom Lane <tgl@sss.pgh.pa.us>
Cc: David Rowley <dgrowleyml@gmail.com>
Cc: pgsql-hackers <pgsql-hackers@postgresql.org>
Subject: Re: Make printtup a bit faster
Date: Wed, 6 May 2026 11:52:10 -0400
Message-ID: <7nfy6enxgwzptesyd2oexlwtqaxtlhbec4yqs44mqgjbyanpss@icwtswsr3wam> (raw)
In-Reply-To: <877bpghevm.fsf@163.com>
References: <87wmjzfz0h.fsf@163.com>
<CAApHDvrBNA-QRsbn-SJyRAsywjHCNenfcbi14c3O3c7=OimQ8Q@mail.gmail.com>
<87bk1aj2go.fsf@163.com>
<CAApHDvqHthJb6baDhgTE5T4RLW6nEX=r239EYmpjfg=Wq5CqQA@mail.gmail.com>
<877bpk7dyg.fsf@163.com>
<117986.1777869518@sss.pgh.pa.us>
<877bpghevm.fsf@163.com>
Hi,
> From Andres:
>
> > FWIW, I've experimented fixing this overhead before, and what I did was to
> > pass an optional context via the fcinfo, and output / send functions could use
> > memory allocated via that optional context object, rather than doing it
> > allocating in CurrentMemoryContext. For the send functions that looks
> > reasonably clean, given that it already deals with a stringinfo. For out
> > functions it's a bit uglier, but still somewhat acceptable.
>
> Puting optional context via the fcinfo looks novel to me (I have zero
> experience to use fcinfo utility.).
We do that in a bunch of places, e.g. for the context of window functions
(c.f. PG_WINDOW_OBJECT() WindowObjectIsValid()).
> Then I'm not sure how to use the optional context, Will it be a
> MemoryContext or a StringInfo? If MemoryContext, then how to avoid the
> memory copy in the printtup sistuation or this method has different target.
I think it'd have to be something that includes the stringinfo.
Here's a very rough prototype for how it could look like. This clearly needs
more helpers that I introduced, but I thought this should be enough to show
the idea.
The first patch is a sketch of something that the second patch depends on, but
that I think we should probably do independently. I'm running working on a
laptop with an almost empty battery, but I'd expect it to be a bit faster than
what we do today.
Greetings,
Andres Freund
Attachments:
[text/x-diff] va1-0001-WIP-Use-permanent-FunctionCallInfo-in-printtup.patch (2.4K, ../7nfy6enxgwzptesyd2oexlwtqaxtlhbec4yqs44mqgjbyanpss@icwtswsr3wam/2-va1-0001-WIP-Use-permanent-FunctionCallInfo-in-printtup.patch)
download | inline diff:
From a3af2227b88eef02db434f2ac66777d4b683a29e Mon Sep 17 00:00:00 2001
From: Andres Freund <andres@anarazel.de>
Date: Wed, 6 May 2026 11:41:36 -0400
Subject: [PATCH va1 1/2] WIP: Use permanent FunctionCallInfo in printtup
Should probably be done similarly for COPY
---
src/backend/access/common/printtup.c | 15 +++++++++++++--
1 file changed, 13 insertions(+), 2 deletions(-)
diff --git a/src/backend/access/common/printtup.c b/src/backend/access/common/printtup.cindex 616bdafd395..6fa93a6798a 100644--- a/src/backend/access/common/printtup.c+++ b/src/backend/access/common/printtup.c@@ -50,6 +50,8 @@ typedef struct
bool typisvarlena; /* is it varlena (ie possibly toastable)? */
int16 format; /* format code for this column */
FmgrInfo finfo; /* Precomputed call info for output fn */
+ /* XXX: Would probably be faster to allocate "inline" */+ FunctionCallInfo outstate; /* Prepared FCI for slightly faster calls */
} PrinttupAttrInfo;
typedef struct
@@ -291,6 +293,10 @@ printtup_prepare_info(DR_printtup *myState, TupleDesc typeinfo, int numAttrs)
ereport(ERROR,
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("unsupported format code: %d", format)));
++ /* both out and send funcs have one argument */+ thisState->outstate = palloc0(SizeForFunctionCallInfo(1));+ thisState->outstate->flinfo = &thisState->finfo;
}
}
@@ -353,12 +359,16 @@ printtup(TupleTableSlot *slot, DestReceiver *self)
VALGRIND_CHECK_MEM_IS_DEFINED(DatumGetPointer(attr),
VARSIZE_ANY(DatumGetPointer(attr)));
+ /* fill in argument for output / send function */+ thisState->outstate->args[0].value = attr;+
if (thisState->format == 0)
{
/* Text output */
char *outputstr;
- outputstr = OutputFunctionCall(&thisState->finfo, attr);+ outputstr = DatumGetCString(FunctionCallInvoke(thisState->outstate));+ Assert(!thisState->outstate->isnull);
pq_sendcountedtext(buf, outputstr, strlen(outputstr));
}
else
@@ -366,7 +376,8 @@ printtup(TupleTableSlot *slot, DestReceiver *self)
/* Binary output */
bytea *outputbytes;
- outputbytes = SendFunctionCall(&thisState->finfo, attr);+ outputbytes = DatumGetByteaP(FunctionCallInvoke(thisState->outstate));+ Assert(!thisState->outstate->isnull);
pq_sendint32(buf, VARSIZE(outputbytes) - VARHDRSZ);
pq_sendbytes(buf, VARDATA(outputbytes),
VARSIZE(outputbytes) - VARHDRSZ);
--
2.46.0.519.g2e7b89e038
[text/x-diff] va1-0002-Mega-WIP-Optimized-out-send-path-for-printtup.patch (7.5K, ../7nfy6enxgwzptesyd2oexlwtqaxtlhbec4yqs44mqgjbyanpss@icwtswsr3wam/3-va1-0002-Mega-WIP-Optimized-out-send-path-for-printtup.patch)
download | inline diff:
From 2bdcbc8bf3b73694fd0bbfbc1907a3197b1129f9 Mon Sep 17 00:00:00 2001
From: Andres Freund <andres@anarazel.de>
Date: Wed, 6 May 2026 11:44:26 -0400
Subject: [PATCH va1 2/2] Mega-WIP: Optimized out/send path for printtup
Discussion: https://postgr.es/m/877bpghevm.fsf@163.com
---
src/include/nodes/miscnodes.h | 12 +++++
src/backend/access/common/printtup.c | 35 +++++++++++--
src/backend/utils/adt/int.c | 73 +++++++++++++++++++++++++---
src/backend/utils/adt/varlena.c | 40 ++++++++++++++-
4 files changed, 148 insertions(+), 12 deletions(-)
diff --git a/src/include/nodes/miscnodes.h b/src/include/nodes/miscnodes.hindex ec833001ab0..b3c189a5c0c 100644--- a/src/include/nodes/miscnodes.h+++ b/src/include/nodes/miscnodes.h@@ -54,4 +54,16 @@ typedef struct ErrorSaveContext
((escontext) != NULL && IsA(escontext, ErrorSaveContext) && \
((ErrorSaveContext *) (escontext))->error_occurred)
++/*+ * Type optionally passed to input/receive/output/send functions that allows+ * those functions to opt into more efficient ways of performing their work+ * (mainly reducing allocations & copies).+ */+typedef struct InOutContext+{+ NodeTag type;+ StringInfo buf;+} InOutContext;+
#endif /* MISCNODES_H */
diff --git a/src/backend/access/common/printtup.c b/src/backend/access/common/printtup.cindex 6fa93a6798a..2e3eb8f56d3 100644--- a/src/backend/access/common/printtup.c+++ b/src/backend/access/common/printtup.c@@ -63,6 +63,7 @@ typedef struct
int nattrs;
PrinttupAttrInfo *myinfo; /* Cached info about each attr */
StringInfoData buf; /* output buffer (*not* in tmpcontext) */
+ InOutContext inout; /* FunctionCallInfo->context data */
MemoryContext tmpcontext; /* Memory context for per-row workspace */
} DR_printtup;
@@ -142,6 +143,9 @@ printtup_startup(DestReceiver *self, int operation, TupleDesc typeinfo)
FetchPortalTargetList(portal),
portal->formats);
+ myState->inout.type = T_InOutContext;+ myState->inout.buf = &myState->buf;+
/* ----------------
* We could set up the derived attr info at this time, but we postpone it
* until the first call of printtup, for 2 reasons:
@@ -297,6 +301,15 @@ printtup_prepare_info(DR_printtup *myState, TupleDesc typeinfo, int numAttrs)
/* both out and send funcs have one argument */
thisState->outstate = palloc0(SizeForFunctionCallInfo(1));
thisState->outstate->flinfo = &thisState->finfo;
++ /*+ * The idea here is that output functions can optionally use more+ * efficient paths if they see that the context is InOutContext, by+ * directly appending correctly formatted output into the output+ * buffer.+ */+ thisState->outstate->context = (Node *) &myState->inout;+ thisState->outstate->nargs = 1;
}
}
@@ -369,7 +382,13 @@ printtup(TupleTableSlot *slot, DestReceiver *self)
outputstr = DatumGetCString(FunctionCallInvoke(thisState->outstate));
Assert(!thisState->outstate->isnull);
- pq_sendcountedtext(buf, outputstr, strlen(outputstr));++ /*+ * If outputstr == NULL, the output function directly appended a+ * correctly formatted message.+ */+ if (outputstr)+ pq_sendcountedtext(buf, outputstr, strlen(outputstr));
}
else
{
@@ -378,9 +397,17 @@ printtup(TupleTableSlot *slot, DestReceiver *self)
outputbytes = DatumGetByteaP(FunctionCallInvoke(thisState->outstate));
Assert(!thisState->outstate->isnull);
- pq_sendint32(buf, VARSIZE(outputbytes) - VARHDRSZ);- pq_sendbytes(buf, VARDATA(outputbytes),- VARSIZE(outputbytes) - VARHDRSZ);++ /*+ * If outputbytes == NULL, the send function directly appended a+ * correctly formatted message.+ */+ if (outputbytes)+ {+ pq_sendint32(buf, VARSIZE(outputbytes) - VARHDRSZ);+ pq_sendbytes(buf, VARDATA(outputbytes),+ VARSIZE(outputbytes) - VARHDRSZ);+ }
}
}
diff --git a/src/backend/utils/adt/int.c b/src/backend/utils/adt/int.cindex 4c894a49d5d..1b7b5b5246c 100644--- a/src/backend/utils/adt/int.c+++ b/src/backend/utils/adt/int.c@@ -327,10 +327,54 @@ Datum
int4out(PG_FUNCTION_ARGS)
{
int32 arg1 = PG_GETARG_INT32(0);
- char *result = (char *) palloc(12); /* sign, 10 digits, '\0' */+ int maxlen = 12; /* sign, 10 digits, '\0' */- pg_ltoa(arg1, result);- PG_RETURN_CSTRING(result);+ if (fcinfo->context && IsA(fcinfo->context, InOutContext))+ {+ /*+ * Optimized path for output functions called as part of a larger+ * ouput.+ *+ * FIXME: A good chunk of this should obviously be in helper+ * functions.+ */+ InOutContext *inout = castNode(InOutContext, fcinfo->context);+ StringInfo buf = inout->buf;+ int prev_buflen;+ int len;+ uint32 len_net;++ /* reserve space for length and the max string length */+ enlargeStringInfo(buf, sizeof(uint32) + maxlen);++ /* reserve space for length, to be filled out later */+ prev_buflen = buf->len;+ buf->len += sizeof(uint32);++ /*+ * Construct string directly in buffer, we don't have to care about+ * encoding conversions, because we assume that every encoding+ * embodies ascii (XXX: Is that actually true with client encodings?).+ */+ len = pg_ltoa(arg1, buf->data + buf->len);+ buf->len += len;++ /* update the previously reserved length */+ len_net = pg_hton32(len);+ memcpy(&buf->data[prev_buflen], &len_net, sizeof(uint32));++ PG_RETURN_VOID();+ }+ else+ {+ /*+ * Fallback path called in any other context.+ */+ char *result = (char *) palloc(maxlen);++ pg_ltoa(arg1, result);+ PG_RETURN_CSTRING(result);+ }
}
/*
@@ -351,11 +395,26 @@ Datum
int4send(PG_FUNCTION_ARGS)
{
int32 arg1 = PG_GETARG_INT32(0);
- StringInfoData buf;- pq_begintypsend(&buf);- pq_sendint32(&buf, arg1);- PG_RETURN_BYTEA_P(pq_endtypsend(&buf));+ if (fcinfo->context && IsA(fcinfo->context, InOutContext))+ {+ InOutContext *inout = castNode(InOutContext, fcinfo->context);++ /* length of data */+ pq_sendint32(inout->buf, 4);+ /* data itself */+ pq_sendint32(inout->buf, arg1);++ PG_RETURN_VOID();+ }+ else+ {+ StringInfoData buf;++ pq_begintypsend(&buf);+ pq_sendint32(&buf, arg1);+ PG_RETURN_BYTEA_P(pq_endtypsend(&buf));+ }
}
diff --git a/src/backend/utils/adt/varlena.c b/src/backend/utils/adt/varlena.cindex c0ff51bd2fc..09913bc01f5 100644--- a/src/backend/utils/adt/varlena.c+++ b/src/backend/utils/adt/varlena.c@@ -290,7 +290,45 @@ textout(PG_FUNCTION_ARGS)
{
Datum txt = PG_GETARG_DATUM(0);
- PG_RETURN_CSTRING(TextDatumGetCString(txt));+ if (fcinfo->context && IsA(fcinfo->context, InOutContext))+ {+ StringInfo buf = castNode(InOutContext, fcinfo->context)->buf;+ text *tunpacked = pg_detoast_datum_packed(DatumGetPointer(txt));+ int len = VARSIZE_ANY_EXHDR(tunpacked);+ char *data = VARDATA_ANY(tunpacked);+ char *data_converted;+ size_t data_len;++ /*+ * Convert text output to the right encoding. For efficiency, this+ * should really happen directly into buf. For that we would have to+ * reserve space for the length first and fill it out after+ * conversion.+ *+ * FIXME: Obviously we would need helpers for this too.+ */+ data_converted = pg_server_to_client(data, len);++ if (data == data_converted)+ data_len = len;+ else+ data_len = strlen(data_converted);++ /* length */+ pq_sendint32(buf, data_len);++ /* actual data */+ appendBinaryStringInfoNT(buf, data_converted, data_len);++ if (tunpacked != DatumGetPointer(txt))+ pfree(tunpacked);++ PG_RETURN_VOID();+ }+ else+ {+ PG_RETURN_CSTRING(TextDatumGetCString(txt));+ }
}
/*
--
2.46.0.519.g2e7b89e038
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Reply to all the recipients using the --to and --cc options:
reply via email
To: pgsql-hackers@postgresql.org
Cc: andres@anarazel.de, zhihuifan1213@163.com, tgl@sss.pgh.pa.us, dgrowleyml@gmail.com
Subject: Re: Make printtup a bit faster
In-Reply-To: <7nfy6enxgwzptesyd2oexlwtqaxtlhbec4yqs44mqgjbyanpss@icwtswsr3wam>
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
This inbox is served by DDX for PostgreSQL; see mirroring instructions
for how to clone and mirror all data and code used for this inbox