On Wed, Sep 23, 2026 at 09:41:36AM -0500, Nathan Bossart 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.
Actually, I think we can expand this to any padding string length by copying the padding string once, and then copying from the beginning of the padding to the end repeatedly so that we write double the padding each time. This is a bit like what commit c60e520 added for pglz_decompress(). I've attached some proof-of-concept grade patches. This doesn't quite match the performance of your 1-byte fast-path, but it's pretty close and applies to many more cases. -- nathan
>From db26e9d41843c6837f3c4ce77c4667bac5c00644 Mon Sep 17 00:00:00 2001 From: Nathan Bossart <[email protected]> Date: Wed, 23 Sep 2026 11:47:02 -0500 Subject: [PATCH v2 1/2] Factor the padding loop out of lpad() and rpad(). lpad() and rpad() each carry an identical copy of the loop that writes the padding characters. This commit moves it into a helper function that both call. No functional change. This is preparatory work for a follow-up commit that will teach the helper to write long padding with a few large memcpy() calls instead of one per character. --- src/backend/utils/adt/oracle_compat.c | 60 ++++++++++++--------------- 1 file changed, 27 insertions(+), 33 deletions(-) diff --git a/src/backend/utils/adt/oracle_compat.c b/src/backend/utils/adt/oracle_compat.c index 7422a454397..2adc3874bc2 100644 --- a/src/backend/utils/adt/oracle_compat.c +++ b/src/backend/utils/adt/oracle_compat.c @@ -143,6 +143,31 @@ casefold(PG_FUNCTION_ARGS) } +/* + * Append m characters of the padding string s2 (s2len bytes) to dst, + * cycling through s2 as needed, and return a pointer past the last byte + * written. + */ +static char * +append_padding(char *dst, const char *s2, int s2len, int m) +{ + const char *ptr2 = s2; + const char *ptr2end = s2 + s2len; + + while (m--) + { + int mlen = pg_mblen_range(ptr2, ptr2end); + + memcpy(dst, ptr2, mlen); + dst += mlen; + ptr2 += mlen; + if (ptr2 == ptr2end) /* wrap around at end of s2 */ + ptr2 = s2; + } + + return dst; +} + /******************************************************************** * * lpad @@ -167,10 +192,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 +231,7 @@ 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 = append_padding(VARDATA(ret), VARDATA_ANY(string2), s2len, m); ptr1 = VARDATA_ANY(string1); @@ -265,10 +274,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 +326,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 = append_padding(ptr_ret, VARDATA_ANY(string2), s2len, m); SET_VARSIZE(ret, ptr_ret - (char *) ret); -- 2.55.0
>From 8bdb4237a3b08b192ddfa7d69128bff3e0b92d47 Mon Sep 17 00:00:00 2001 From: Nathan Bossart <[email protected]> Date: Wed, 23 Sep 2026 11:47:23 -0500 Subject: [PATCH v2 2/2] Optimize padding in lpad() and rpad(). lpad() and rpad() write the padding one character at a time, calling pg_mblen_range() and memcpy() for each one, which gets slow once the padding runs to many kilobytes. Since the padding is just the pad string repeated, this commit copies the pad string once, as before, and then produces the rest by copying what has already been written onto the end of itself, doubling the length each time. That takes a handful of memcpy() calls for a pad string of any length, and it is dramatically faster for long padding. Note that the pad string is still only checked for a truncated multibyte character as far as it is actually used, so a bad tail is accepted when no padding is needed, as before. Co-authored-by: Sehrope Sarkuni <[email protected]> Discussion: https://postgr.es/m/CAH7T-apj%2BpFg9bRkXVGfGejS2Uu4WzdMd7KoLKTW11mdsrt1Ew%40mail.gmail.com --- src/backend/utils/adt/oracle_compat.c | 45 ++++++++++++++++++++++++-- src/test/regress/expected/encoding.out | 19 +++++++++++ src/test/regress/sql/encoding.sql | 7 ++++ 3 files changed, 68 insertions(+), 3 deletions(-) diff --git a/src/backend/utils/adt/oracle_compat.c b/src/backend/utils/adt/oracle_compat.c index 2adc3874bc2..d54fe948355 100644 --- a/src/backend/utils/adt/oracle_compat.c +++ b/src/backend/utils/adt/oracle_compat.c @@ -147,22 +147,61 @@ casefold(PG_FUNCTION_ARGS) * Append m characters of the padding string s2 (s2len bytes) to dst, * cycling through s2 as needed, and return a pointer past the last byte * written. + * + * Note that s2 is only checked for a truncated multibyte character as far as + * we actually use it, so a bad tail is accepted when the padding stops short + * of it, as it always has been. */ static char * append_padding(char *dst, const char *s2, int s2len, int m) { const char *ptr2 = s2; const char *ptr2end = s2 + s2len; + char *start = dst; + int nchars = 0; + int nbytes; + int written; - while (m--) + /* Copy s2 once, one character at a time, or until m runs out. */ + while (m > 0 && ptr2 < ptr2end) { int mlen = pg_mblen_range(ptr2, ptr2end); memcpy(dst, ptr2, mlen); dst += mlen; ptr2 += mlen; - if (ptr2 == ptr2end) /* wrap around at end of s2 */ - ptr2 = s2; + m--; + nchars++; + } + + if (m == 0) + return dst; + + /* + * The rest of the padding is s2 repeated, so work out how many bytes that + * is: whole copies of s2, plus the first m % nchars characters of one + * more. + */ + nbytes = (m / nchars) * s2len; + ptr2 = s2; + for (m %= nchars; m > 0; m--) + ptr2 += pg_mblen_unbounded(ptr2); + nbytes += ptr2 - s2; + + /* + * Now produce those bytes by copying what we've already written, doubling + * the length each time, so that the work is done by a few large memcpy() + * calls rather than one per character. + */ + written = dst - start; + while (nbytes > 0) + { + int chunk = Min(written, nbytes); + + memcpy(dst, start, chunk); + dst += chunk; + nbytes -= chunk; + written += chunk; } return dst; 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.55.0
