On Wed, Sep 23, 2026 at 10:41 AM Nathan Bossart
<[email protected]> wrote:
>
> On Wed, Sep 23, 2026 at 10:10:09AM -0400, Sehrope Sarkuni wrote:
> > 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().
>
> I wonder if we could expand these gains by using SIMD whenever the vector
> length is divisible by the padding string length.  My hunch is that's where
> a lot of the memset() gains come from.

I think the repeated pg_mblen_range() calls for every iteration are a
big factor too.  I tried a different route that covers every pad
string (not just divisible) without explicit SIMD.

The attached v2 walks the pad string once to count its characters,
copies the whole repetitions as byte sequences, and runs the
per-character loop only for the partial final repetition.  The whole
repetitions are written by copying one and then doubling the copied
region, so it takes log2(repetitions) memcpy() calls and memcpy()
does the vectorizing.

Validation is unchanged from master.  The counting pass stops at the
number of characters needed, so a pad string ending in a lone lead
byte still errors only if the padding reaches that byte.  The two
identical loops in lpad() and rpad() become a static pad_fill().

This replaces the memset() patch.  On the one-byte case memset() was
5-7% faster than doubling (0.100 vs 0.107 ms at 1M), which does not
seem worth a separate fast path, and the multi-character controls that
were 5% slower with v1 now get the same speedup as everything else.

Same setup as before, median ms of 5 rounds:

  SELECT octet_length(rpad('x', N, PAD))

  PAD                N          before    after
  ' '                1000000     4.852    0.108
  'ab'               1000000     4.822    0.111
  'abc'               999999     4.824    0.107   (exact multiple)
  'abc'              1000000     4.793    0.108   (partial tail)
  'abcd'             1000000     4.809    0.110
  16 chars           1000000     5.012    0.107
  100 chars          1000000     4.917    0.110
  'é'                1000000     5.394    0.155
  'aéb'               999999     5.012    0.126   (exact multiple)
  'aéb'              1000000     4.994    0.122   (partial tail)
  ' '               10000000    53.705    5.466
  'ab'              10000000    53.376    5.413
  'abc'                 1000     0.063    0.057
  'abc'                   10     0.056    0.056

This is more of a change than just adding the memset() fastpath, but I
think the end result of the code is easier to follow too with both
lpad() and rpad() sharing the helper.

Regards,
-- Sehrope Sarkuni
Founder & CEO | JackDB, Inc. | https://www.jackdb.com/
From 512b8c05da4d7db8108a3ea9234f09307d35ed15 Mon Sep 17 00:00:00 2001
From: Sehrope Sarkuni <[email protected]>
Date: Wed, 23 Sep 2026 15:29:23 +0000
Subject: [PATCH v2] Copy whole repetitions of the pad string at once in lpad()
 and rpad()

The padding loop called pg_mblen_range() and memcpy() once per
character.  Walk the pad string once to count its characters, copy the
whole repetitions as byte sequences by doubling the copied region, and
use the per-character loop only for the partial final repetition.

The pad string is validated as far as before, which is all of it when
a full repetition is copied and otherwise only the characters copied.
---
 src/backend/utils/adt/oracle_compat.c  | 101 +++++++++++++++++--------
 src/test/regress/expected/encoding.out |  37 +++++++++
 src/test/regress/expected/strings.out  |  26 +++++++
 src/test/regress/sql/encoding.sql      |  10 +++
 src/test/regress/sql/strings.sql       |   7 ++
 5 files changed, 150 insertions(+), 31 deletions(-)

diff --git a/src/backend/utils/adt/oracle_compat.c b/src/backend/utils/adt/oracle_compat.c
index 7422a454397..c2c58f28e38 100644
--- a/src/backend/utils/adt/oracle_compat.c
+++ b/src/backend/utils/adt/oracle_compat.c
@@ -143,6 +143,74 @@ casefold(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Write m characters of padding at dst, taken cyclically from the pad
+ * string of padlen bytes, and return the number of bytes written.
+ *
+ * The pad string is validated with pg_mblen_range() only as far as it is
+ * used, so an incomplete multibyte character at its end is an error only
+ * if the padding reaches it.
+ */
+static int
+pad_fill(char *dst, const char *pad, int padlen, int m)
+{
+	const char *padend = pad + padlen;
+	const char *p = pad;
+	int			nchars = 0;
+	int			nrep;
+	int			total;
+	int			copied;
+
+	if (m <= 0)
+		return 0;
+
+	/* count the characters of one repetition, stopping at m */
+	while (p < padend && nchars < m)
+	{
+		p += pg_mblen_range(p, padend);
+		nchars++;
+	}
+
+	/* fewer than one repetition is needed, so copy the first m characters */
+	if (p < padend)
+	{
+		memcpy(dst, pad, p - pad);
+		return p - pad;
+	}
+
+	/*
+	 * Whole repetitions are byte copies of the pad string.  Copy one, then
+	 * double the copied region until all of them are written, so the number
+	 * of memcpy() calls is logarithmic in the number of repetitions.  total
+	 * cannot overflow, since the caller sized the output for m characters.
+	 */
+	nrep = m / nchars;
+	total = nrep * padlen;
+	memcpy(dst, pad, padlen);
+	copied = padlen;
+	while (copied < total)
+	{
+		int			n = Min(copied, total - copied);
+
+		memcpy(dst + copied, dst, n);
+		copied += n;
+	}
+
+	/* partial final repetition, one character at a time */
+	m -= nrep * nchars;
+	p = pad;
+	while (m-- > 0)
+	{
+		int			mlen = pg_mblen_range(p, padend);
+
+		memcpy(dst + copied, p, mlen);
+		copied += mlen;
+		p += mlen;
+	}
+
+	return copied;
+}
+
 /********************************************************************
  *
  * lpad
@@ -167,10 +235,7 @@ lpad(PG_FUNCTION_ARGS)
 	text	   *string2 = PG_GETARG_TEXT_PP(2);
 	text	   *ret;
 	char	   *ptr1,
-			   *ptr2,
-			   *ptr2start,
 			   *ptr_ret;
-	const char *ptr2end;
 	int			m,
 				s1len,
 				s2len;
@@ -209,20 +274,9 @@ lpad(PG_FUNCTION_ARGS)
 
 	m = len - s1len;
 
-	ptr2 = ptr2start = VARDATA_ANY(string2);
-	ptr2end = ptr2 + s2len;
 	ptr_ret = VARDATA(ret);
 
-	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;
-	}
+	ptr_ret += pad_fill(ptr_ret, VARDATA_ANY(string2), s2len, m);
 
 	ptr1 = VARDATA_ANY(string1);
 
@@ -265,10 +319,7 @@ rpad(PG_FUNCTION_ARGS)
 	text	   *string2 = PG_GETARG_TEXT_PP(2);
 	text	   *ret;
 	char	   *ptr1,
-			   *ptr2,
-			   *ptr2start,
 			   *ptr_ret;
-	const char *ptr2end;
 	int			m,
 				s1len,
 				s2len;
@@ -320,19 +371,7 @@ rpad(PG_FUNCTION_ARGS)
 		ptr1 += mlen;
 	}
 
-	ptr2 = ptr2start = VARDATA_ANY(string2);
-	ptr2end = ptr2 + s2len;
-
-	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;
-	}
+	ptr_ret += pad_fill(ptr_ret, VARDATA_ANY(string2), s2len, m);
 
 	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..9fc871215b0 100644
--- a/src/test/regress/expected/encoding.out
+++ b/src/test/regress/expected/encoding.out
@@ -60,6 +60,43 @@ SELECT reverse(good) FROM regress_encoding;
  éfac
 (1 row)
 
+-- multibyte pad strings: whole repetitions, a partial final repetition, and
+-- fewer than one repetition
+SELECT lpad(good, 7, 'é'), rpad(good, 7, 'é') FROM regress_encoding;
+  lpad   |  rpad   
+---------+---------
+ ééécafé | caféééé
+(1 row)
+
+SELECT lpad(good, 12, 'éab'), rpad(good, 12, 'éab') FROM regress_encoding;
+     lpad     |     rpad     
+--------------+--------------
+ éabéabéacafé | cafééabéabéa
+(1 row)
+
+SELECT lpad(good, 5, 'éab'), rpad(good, 5, 'éab') FROM regress_encoding;
+ lpad  | rpad  
+-------+-------
+ écafé | caféé
+(1 row)
+
+-- a lone lead byte in the pad string is an error if the padding reaches it
+SELECT lpad(good, 7, test_bytea_to_text('\xc3')) FROM regress_encoding;
+ERROR:  invalid byte sequence for encoding "UTF8": 0xc3
+SELECT rpad(good, 7, 'ab' || test_bytea_to_text('\xc3')) FROM regress_encoding;
+ERROR:  invalid byte sequence for encoding "UTF8": 0xc3
+SELECT lpad(good, 5, 'ab' || test_bytea_to_text('\xc3')), rpad(good, 6, 'ab' || test_bytea_to_text('\xc3')) FROM regress_encoding;
+ lpad  |  rpad  
+-------+--------
+ acafé | caféab
+(1 row)
+
+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/expected/strings.out b/src/test/regress/expected/strings.out
index fa29abfd829..6e064e3807f 100644
--- a/src/test/regress/expected/strings.out
+++ b/src/test/regress/expected/strings.out
@@ -3441,6 +3441,32 @@ SELECT rpad('hi', 5, '');
  hi
 (1 row)
 
+-- whole repetitions of the pad string, a partial final repetition, and
+-- fewer than one repetition
+SELECT lpad('hi', 8, 'abc'), rpad('hi', 8, 'abc');
+   lpad   |   rpad   
+----------+----------
+ abcabchi | hiabcabc
+(1 row)
+
+SELECT lpad('hi', 9, 'abc'), rpad('hi', 9, 'abc');
+   lpad    |   rpad    
+-----------+-----------
+ abcabcahi | hiabcabca
+(1 row)
+
+SELECT lpad('hi', 12, 'ab'), rpad('hi', 12, 'ab');
+     lpad     |     rpad     
+--------------+--------------
+ abababababhi | hiababababab
+(1 row)
+
+SELECT lpad('hi', 3, 'abc'), rpad('hi', 3, 'abc');
+ lpad | rpad 
+------+------
+ ahi  | hia
+(1 row)
+
 SELECT ltrim('zzzytrim', 'xyz');
  ltrim 
 -------
diff --git a/src/test/regress/sql/encoding.sql b/src/test/regress/sql/encoding.sql
index 26caa93a5d5..3ea6e54e52d 100644
--- a/src/test/regress/sql/encoding.sql
+++ b/src/test/regress/sql/encoding.sql
@@ -37,6 +37,16 @@ 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;
+-- multibyte pad strings: whole repetitions, a partial final repetition, and
+-- fewer than one repetition
+SELECT lpad(good, 7, 'é'), rpad(good, 7, 'é') FROM regress_encoding;
+SELECT lpad(good, 12, 'éab'), rpad(good, 12, 'éab') FROM regress_encoding;
+SELECT lpad(good, 5, 'éab'), rpad(good, 5, 'éab') FROM regress_encoding;
+-- a lone lead byte in the pad string is an error if the padding reaches it
+SELECT lpad(good, 7, test_bytea_to_text('\xc3')) FROM regress_encoding;
+SELECT rpad(good, 7, 'ab' || test_bytea_to_text('\xc3')) FROM regress_encoding;
+SELECT lpad(good, 5, 'ab' || test_bytea_to_text('\xc3')), rpad(good, 6, 'ab' || test_bytea_to_text('\xc3')) FROM regress_encoding;
+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;
diff --git a/src/test/regress/sql/strings.sql b/src/test/regress/sql/strings.sql
index 7d9c7275a02..04651a0a46f 100644
--- a/src/test/regress/sql/strings.sql
+++ b/src/test/regress/sql/strings.sql
@@ -1165,6 +1165,13 @@ SELECT rpad('hi', -5, 'xy');
 SELECT rpad('hello', 2);
 SELECT rpad('hi', 5, '');
 
+-- whole repetitions of the pad string, a partial final repetition, and
+-- fewer than one repetition
+SELECT lpad('hi', 8, 'abc'), rpad('hi', 8, 'abc');
+SELECT lpad('hi', 9, 'abc'), rpad('hi', 9, 'abc');
+SELECT lpad('hi', 12, 'ab'), rpad('hi', 12, 'ab');
+SELECT lpad('hi', 3, 'abc'), rpad('hi', 3, 'abc');
+
 SELECT ltrim('zzzytrim', 'xyz');
 
 SELECT translate('', '14', 'ax');
-- 
2.43.0

Reply via email to