Hi hackers,

lpad() and rpad() pad one character at a time, calling
pg_mblen_range() and memcpy() once per padding char.  When the padding
string is a single byte, e.g., lpad(x, n, '0') or rpad(x, n, ' '),
the padding is that byte repeated, so the attached patch fills it with
one memset().

pg_mblen_range() is still called once on the byte, so a lone lead byte
of a multibyte character is rejected as before.  The fast path is
skipped when no padding is needed, so such a byte is still accepted in
that case.  Multibyte and multi-character padding strings still use
the loop.

One oddity of the current implementation I noticed is that the pad
string is only validated when it is being copied.  So an invalid pad
string is accepted when no padding is needed.  This patch preserves
that behavior but it seemed weird enough (to me) to mention here.

The existing strings.sql tests already run the one-byte path through
the default space padding.  The patch adds tests to encoding.sql,
for padding with a multibyte character and for the lone-lead-byte
cases.

Timings on an AMD Ryzen 7 5700G, release build (-O3, no asserts),
pgbench -c 1 with one statement per transaction, alternating
before/after rounds, median latency in ms of 5 rounds (3 at 100M):

  SELECT octet_length(rpad('x', N, ' '))
    or
  SELECT octet_length(lpad('x', N, '0'))

  N              rpad before   after     lpad before   after
  1                  0.056     0.056         0.057     0.056
  10                 0.056     0.056         0.057     0.056
  100                0.056     0.056         0.057     0.057
  1000               0.062     0.056         0.062     0.056
  10000              0.106     0.058         0.106     0.057
  100000             0.543     0.061         0.548     0.061
  1000000            4.799     0.102         4.805     0.102
  10000000          53.415     5.350        53.773     5.194
  100000000        567.672    96.637       567.826    96.564

 controls (generic loop):
  octet_length(rpad('x', 1000000, 'é'))     5.342     5.676
  octet_length(lpad('x', 1000000, 'ab'))    4.841     5.078

The generic-loop controls are consistently about 5-6% slower. I
tried a few arrangements of the if-block, but they produced the same
code layout. The generic loop now straddles a 64-byte boundary where
it fit within one before. I wasn't able to eliminate that difference
by rearranging the fast path.

Large pads with a single byte seem far more common than large pads
with a multi-byte or multi-character string.

Passes check-world with asserts enabled.

Regards,
-- Sehrope Sarkuni
Founder & CEO | JackDB, Inc. | https://www.jackdb.com/
From 4506127f660e28aba45ce5d69169eaaea1f1c4bb Mon Sep 17 00:00:00 2001
From: Sehrope Sarkuni <[email protected]>
Date: Tue, 22 Sep 2026 13:36:03 +0000
Subject: [PATCH] Use memset() for one-byte padding in lpad() and rpad()

A one-byte padding string is that byte repeated, so fill the padding
with memset() instead of the per-character loop, which called
pg_mblen_range() and memcpy() once per output byte.

pg_mblen_range() is still called once on the byte, so a lone lead byte
of a multibyte character is rejected as before.  The fast path is
skipped when there is nothing to pad, so such a byte is still accepted
in that case.
---
 src/backend/utils/adt/oracle_compat.c  | 53 +++++++++++++++++++-------
 src/test/regress/expected/encoding.out | 19 +++++++++
 src/test/regress/sql/encoding.sql      |  7 ++++
 3 files changed, 65 insertions(+), 14 deletions(-)

diff --git a/src/backend/utils/adt/oracle_compat.c b/src/backend/utils/adt/oracle_compat.c
index 7422a454397..5e9459a364d 100644
--- a/src/backend/utils/adt/oracle_compat.c
+++ b/src/backend/utils/adt/oracle_compat.c
@@ -213,15 +213,30 @@ lpad(PG_FUNCTION_ARGS)
 	ptr2end = ptr2 + s2len;
 	ptr_ret = VARDATA(ret);
 
-	while (m--)
+	if (s2len == 1 && m > 0)
 	{
-		int			mlen = pg_mblen_range(ptr2, ptr2end);
+		/*
+		 * A one-byte padding string is a single character repeated m times,
+		 * so fill it in with one memset() rather than one memcpy() per
+		 * character.  pg_mblen_range() is still called once so that a lone
+		 * lead byte of a multibyte character is rejected as before.
+		 */
+		(void) pg_mblen_range(ptr2, ptr2end);
+		memset(ptr_ret, *ptr2, m);
+		ptr_ret += m;
+	}
+	else
+	{
+		while (m--)
+		{
+			int			mlen = pg_mblen_range(ptr2, ptr2end);
 
-		memcpy(ptr_ret, ptr2, mlen);
-		ptr_ret += mlen;
-		ptr2 += mlen;
-		if (ptr2 == ptr2end)	/* wrap around at end of s2 */
-			ptr2 = ptr2start;
+			memcpy(ptr_ret, ptr2, mlen);
+			ptr_ret += mlen;
+			ptr2 += mlen;
+			if (ptr2 == ptr2end)	/* wrap around at end of s2 */
+				ptr2 = ptr2start;
+		}
 	}
 
 	ptr1 = VARDATA_ANY(string1);
@@ -323,15 +338,25 @@ rpad(PG_FUNCTION_ARGS)
 	ptr2 = ptr2start = VARDATA_ANY(string2);
 	ptr2end = ptr2 + s2len;
 
-	while (m--)
+	if (s2len == 1 && m > 0)
+	{
+		/* Same one-byte padding fast path as in lpad() */
+		(void) pg_mblen_range(ptr2, ptr2end);
+		memset(ptr_ret, *ptr2, m);
+		ptr_ret += m;
+	}
+	else
 	{
-		int			mlen = pg_mblen_range(ptr2, ptr2end);
+		while (m--)
+		{
+			int			mlen = pg_mblen_range(ptr2, ptr2end);
 
-		memcpy(ptr_ret, ptr2, mlen);
-		ptr_ret += mlen;
-		ptr2 += mlen;
-		if (ptr2 == ptr2end)	/* wrap around at end of s2 */
-			ptr2 = ptr2start;
+			memcpy(ptr_ret, ptr2, mlen);
+			ptr_ret += mlen;
+			ptr2 += mlen;
+			if (ptr2 == ptr2end)	/* wrap around at end of s2 */
+				ptr2 = ptr2start;
+		}
 	}
 
 	SET_VARSIZE(ret, ptr_ret - (char *) ret);
diff --git a/src/test/regress/expected/encoding.out b/src/test/regress/expected/encoding.out
index 0bb72a1df6f..8098599e302 100644
--- a/src/test/regress/expected/encoding.out
+++ b/src/test/regress/expected/encoding.out
@@ -60,6 +60,25 @@ SELECT reverse(good) FROM regress_encoding;
  éfac
 (1 row)
 
+-- padding with a multibyte character
+SELECT lpad(good, 7, 'é'), rpad(good, 7, 'é') FROM regress_encoding;
+  lpad   |  rpad   
+---------+---------
+ ééécafé | caféééé
+(1 row)
+
+-- padding with a lone lead byte of a multibyte character = error
+SELECT lpad(good, 7, test_bytea_to_text('\xc3')) FROM regress_encoding;
+ERROR:  invalid byte sequence for encoding "UTF8": 0xc3
+SELECT rpad(good, 7, test_bytea_to_text('\xc3')) FROM regress_encoding;
+ERROR:  invalid byte sequence for encoding "UTF8": 0xc3
+-- no error when no padding is needed
+SELECT lpad(good, 4, test_bytea_to_text('\xc3')), rpad(good, 4, test_bytea_to_text('\xc3')) FROM regress_encoding;
+ lpad | rpad 
+------+------
+ café | café
+(1 row)
+
 -- invalid short mb character = error
 SELECT length(truncated) FROM regress_encoding;
 ERROR:  invalid byte sequence for encoding "UTF8": 0xc3
diff --git a/src/test/regress/sql/encoding.sql b/src/test/regress/sql/encoding.sql
index 26caa93a5d5..7121704f790 100644
--- a/src/test/regress/sql/encoding.sql
+++ b/src/test/regress/sql/encoding.sql
@@ -37,6 +37,13 @@ SELECT substring(good, 3, 1) FROM regress_encoding;
 SELECT substring(good, 4, 1) FROM regress_encoding;
 SELECT regexp_replace(good, '^caf(.)$', '\1') FROM regress_encoding;
 SELECT reverse(good) FROM regress_encoding;
+-- padding with a multibyte character
+SELECT lpad(good, 7, 'é'), rpad(good, 7, 'é') FROM regress_encoding;
+-- padding with a lone lead byte of a multibyte character = error
+SELECT lpad(good, 7, test_bytea_to_text('\xc3')) FROM regress_encoding;
+SELECT rpad(good, 7, test_bytea_to_text('\xc3')) FROM regress_encoding;
+-- no error when no padding is needed
+SELECT lpad(good, 4, test_bytea_to_text('\xc3')), rpad(good, 4, test_bytea_to_text('\xc3')) FROM regress_encoding;
 
 -- invalid short mb character = error
 SELECT length(truncated) FROM regress_encoding;
-- 
2.17.1

Reply via email to