On Tue, Jun 30, 2026 at 11:03 AM Haibo Yan <[email protected]> wrote:
>
> On Tue, Jun 30, 2026 at 10:53 AM Masahiko Sawada <[email protected]>
> wrote:
> >
> > On Mon, Jun 29, 2026 at 5:53 PM Haibo Yan <[email protected]> wrote:
> > >
> > > On Mon, Jun 29, 2026 at 2:55 PM Masahiko Sawada <[email protected]>
> > > wrote:
> > > >
> > > > On Sun, Jun 28, 2026 at 7:20 PM Haibo Yan <[email protected]> wrote:
> > > > >
> > > > > On Thu, Jun 25, 2026 at 3:16 PM Masahiko Sawada
> > > > > <[email protected]> wrote:
> > > > > >
> > > > > > On Thu, Jun 25, 2026 at 2:31 PM Haibo Yan <[email protected]>
> > > > > > wrote:
> > > > > > >
> > > > > > >
> > > > > > >
> > > > > > > On Thu, Jun 25, 2026 at 11:28 AM Masahiko Sawada
> > > > > > > <[email protected]> wrote:
> > > > > > >>
> > > > > > >> Hi all,
> > > > > > >>
> > > > > > >> I'd like to propose the $subject.
> > > > > > >>
> > > > > > >> Since commit ec8719ccbfcd made hex_decode_safe() SIMD-aware,
> > > > > > >> decoding
> > > > > > >> a run of hex digits is now fast. The attached patch reuses
> > > > > > >> hex_decode_safe() in the UUID input function to speed up parsing.
> > > > > > >>
> > > > > > >> We accept several textual forms of a UUID[1]. The fast path
> > > > > > >> handles
> > > > > > >> the common ones: 32 hex digits, the canonical 8x-4x-4x-4x-12x
> > > > > > >> form
> > > > > > >> (where "nx" means n hex digits), and either of those wrapped in
> > > > > > >> braces. Otherwise, it falls back to the ordinary scalar UUID
> > > > > > >> parse.
> > > > > > >>
> > > > > > >> I've benchmarked the parse speed using the following query:
> > > > > > >>
> > > > > > >> CREATE TEMP TABLE u AS SELECT gen_random_uuid()::text AS t FROM
> > > > > > >> generate_series(1, 1000000);
> > > > > > >> EXPLAIN (ANALYZE, TIMING OFF) SELECT t::uuid FROM u;
> > > > > > >>
> > > > > > >> I compared the execution time of the second query, which measures
> > > > > > >> uuid_in() alone, with/without SIMD optimization. Here are
> > > > > > >> results (the
> > > > > > >> median of 5 runs):
> > > > > > >>
> > > > > > >> HEAD: 208.879 ms
> > > > > > >> Patched: 40.983 ms
> > > > > > >>
> > > > > > >> The improvements look promising to me. But in a realistic
> > > > > > >> pipeline the
> > > > > > >> parse is a small fraction of the work, so end-to-end gains could
> > > > > > >> be
> > > > > > >> much smaller.
> > > > > > >>
> > > > > > >> Feedback is very welcome.
> > > > > > >>
> > > > > > > I may be missing something, but I wonder whether the fast path is
> > > > > > > relying on
> > > > > > > slightly different input semantics from the existing UUID parser.
> > > > > > >
> > > > > > > In particular, hex_decode_safe() is not a strict “32 hex
> > > > > > > characters only”
> > > > > > > decoder. It skips whitespace, which is fine for its existing
> > > > > > > callers, but I
> > > > > > > don’t think UUID input should treat whitespace inside the UUID
> > > > > > > body as
> > > > > > > ignorable.
> > > > > >
> > > > > > Good catch! hex_decode_safe() skips whitespaces so the patch accepts
> > > > > > the following UUID value, which is bad:
> > > > > >
> > > > > > select '019f00b5-7f8a-722f-b707-59f0ed25cd '::uuid;
> > > > > > uuid
> > > > > > --------------------------------------
> > > > > > 019f00b5-7f8a-722f-b707-59f0ed25cd00
> > > > > > (1 row)
> > > > > >
> > > > > > > Also, since hex_decode_safe() returns void, the UUID fast path
> > > > > > > cannot verify that exactly UUID_LEN bytes were produced.
> > > > > >
> > > > > > IIUC hex_decode_safe() does return the output length in bytes. So I
> > > > > > think we can fallback to the scalar UUID parser if
> > > > > > esctx.error_occurred is true or if the returned value is not 16.
> > > > > >
> > > > >
> > > > > You’re right, I misread that part. Checking both
> > > > > esctx.error_occurred and
> > > > > the returned length sounds good to me.
> > > > >
> > > > > > >
> > > > > > > So I think it would be safer either to pre-validate that the 32
> > > > > > > source
> > > > > > > characters are all hex digits before calling hex_decode_safe(),
> > > > > > > or to use a
> > > > > > > UUID-specific strict hex decoder for this path. After that, a
> > > > > > > comment
> > > > > > > explaining why hex_decode_safe() is safe here would make the
> > > > > > > invariant much
> > > > > > > clearer.
> > > > > >
> > > > > > IIUC hex_decode_simd_helper() accepts only hex digits so we could
> > > > > > re-use it for UUID parsing. Let me check if the above idea of using
> > > > > > the return value works for us first.
> > > > > >
> > > > >
> > > > > That sounds reasonable. My main concern was to keep the fast path’s
> > > > > accepted
> > > > > input set identical to the scalar UUID parser. Falling back when the
> > > > > decoded
> > > > > length is not UUID_LEN, together with regression tests for whitespace
> > > > > cases,
> > > > > should address that.
> > > > >
> > > > > > >
> > > > > > > Could you also add a few regression tests for invalid inputs that
> > > > > > > contain
> > > > > > > whitespace inside otherwise fast-path-looking UUID strings? For
> > > > > > > example:
> > > > > > >
> > > > > > > ---------------------------------------------------------------
> > > > > > >
> > > > > > > SELECT 'a0eebc99 9c0b4ef8bb6d6bb9bd380a11'::uuid;
> > > > > > > SELECT 'a0eebc999c0b4ef8bb6d6bb9bd380a1 '::uuid;
> > > > > > > SELECT '{a0eebc999c0b4ef8bb6d6bb9bd380a1 }'::uuid;
> > > > > > > SELECT 'a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a1 '::uuid;
> > > > > > > ---------------------------------------------------------------
> > > > > > >
> > > > > > > These should continue to be rejected in the same way as the
> > > > > > > scalar parser.
> > > > > > > Regards,
> > > > > >
> > > > > > Agreed.
> > > > > >
> > > >
> > > > I've attached the updated patch.
> > > >
> > > > Regards,
> > > >
> > > > --
> > > > Masahiko Sawada
> > > > Amazon Web Services: https://aws.amazon.com
> > >
> > > I noticed a few typos in the comments:
> > >
> > > src/backend/utils/adt/uuid.c
> > > line 56: “scalar implmentation” -> “scalar implementation”
> > > line 109: “swalled” -> “swallowed”
> > > line 110: “kepping” -> “keeping”
> > > line 118: “grammer” -> “grammar”
> > > line 119: “whitespaces” -> “whitespace”
> > >
> > > Could you fix them ?
> >
> > Oops, I fixed them and rechecked other places.
> >
> > I've attached the updated patch.
> >
> > Regards,
> >
> > --
> > Masahiko Sawada
> > Amazon Web Services: https://aws.amazon.com
>
> The code looks good to me now. I only noticed one small typo in the
> commit trailer: Reviwed-by should be Reviewed-by.
>
> Otherwise, it looks good. Thank you for fixing these issues.
>
After spending more time on this patch, I find out two things:
1. USE_NO_SIMD doesn't work in uuid.c without including port/simd.h.
But including port/simd.h seems wrong as it doesn't use any SIMD
support functions.
2. hex_decode_safe() is faster than the current UUID parse
(isxdigit()+strtoul() approach) even without SIMD. I've created a
small benchmark test tool (attached as 0002 patch, not intended to be
pushed into the core), and measures UUID parsing performance of three
approaches: 'scalar' is the current string_to_uuid() that uses
isxdigit()+strtoul()), 'simd' uses hex_decode_safe() with SIMD, and
'nosimd' uses hex_decode_safe() without SIMD, with different shapes of
UUIDs. Here are results:
=# select path, shape, n_inputs, best_ms::numeric(10,3) from
uuid_parse_bench(100000, 5);
path | shape | n_inputs | best_ms
--------+------------------+----------+---------
scalar | canonical | 100000 | 22.661
simd | canonical | 100000 | 1.400
nosimd | canonical | 100000 | 1.652
scalar | bare32 | 100000 | 15.932
simd | bare32 | 100000 | 0.471
nosimd | bare32 | 100000 | 1.110
scalar | braced_canonical | 100000 | 17.330
simd | braced_canonical | 100000 | 1.088
nosimd | braced_canonical | 100000 | 1.314
scalar | braced_bare32 | 100000 | 15.942
simd | braced_bare32 | 100000 | 0.488
nosimd | braced_bare32 | 100000 | 1.141
scalar | dashed4 | 100000 | 16.185
simd | dashed4 | 100000 | 16.493
nosimd | dashed4 | 100000 | 16.403
scalar | invalid_hex | 100000 | 0.199
simd | invalid_hex | 100000 | 1.150
nosimd | invalid_hex | 100000 | 0.385
(18 rows)
Each of shape means:
- 'canonical': 8x-4x-4x-4x-12x, what uuid_out() emits
- 'bare32': 32 contiguous hex digits
- 'braced_canonical': {8x-4x-4x-4x-12x}
- 'braced_bare32': {32 hex digits}
- 'dashed4': dash after every group of 4
- 'invalid_hdx': canonical but with a invalid digit
'nosimd' is 10x~ faster than 'scalar' in most cases. All paths are
mostly the same in 'dashed4' and 'invalid_hex' cases because 'simd'
and 'nosimd' fall back to the 'scalar' case. According to these
results, my conclusion is that we can use hex_decode_safe() for
canonical forms and 32 contiguous hex forms anyway, and let
hex_decode_safe() choose whether to use SIMD. We would win in either
case. We still use the current scalar approach for uncommon UUID forms
and error reporting purposes.
Regards,
--
Masahiko Sawada
Amazon Web Services: https://aws.amazon.com
From 9d2edad34a1823bab012f5d0593d461a9ec396fb Mon Sep 17 00:00:00 2001
From: Masahiko Sawada <[email protected]>
Date: Thu, 25 Jun 2026 10:03:44 -0700
Subject: [PATCH v4 1/2] Optimize UUID parse using SIMD.
Previously, string_to_uuid() decoded one byte at a time, calling
isxdigit() twice and strtoul() once for every pair of hexadecimal
digits. That loop dominated the cost of uuid_in().
This commit adds a fast path for the two common shapes: a bare string
of 32 hexadecimal digits, and the canonical 8x-4x-4x-4x-12x form
(where "nx" means n hexadecimal digits), each optionally wrapped in
braces. Both are compacted into 32 contiguous hexadecimal digits and
decoded with hex_decode_safe(). Any other shape, or any decoding
error, is handed off to the original scalar parser, now
string_to_uuid_scalar(), so the accepted grammar and the error
messages are unchanged.
hex_decode_safe() silently skips whitespace while the UUID grammar
does not, so a decode can succeed and still write fewer than UUID_LEN
bytes. The fast path therefore treats a short result as a failure,
just like an error, and lets the scalar parser reject the input and
report the syntax error.
The fast path is deliberately not conditional on SIMD support.
hex_decode_safe() selects a vectorized or scalar implementation
itself, and even its scalar implementation is an order of magnitude
faster than decoding a byte at a time, so gating this on USE_NO_SIMD
would only penalize platforms that have neither SSE2 nor NEON.
Reviewed-by: Bharath Rupireddy <[email protected]>
Reviewed-by: Haibo Yan <[email protected]>
Discussion: https://postgr.es/m/cad21aocqer4uqu77q_yomnnzj7aveio5qzt+4hnzpm4wm-e...@mail.gmail.com
---
src/backend/utils/adt/uuid.c | 94 ++++++++++++++++++++++++++++--
src/test/regress/expected/uuid.out | 55 +++++++++++++++++
src/test/regress/sql/uuid.sql | 16 +++++
3 files changed, 160 insertions(+), 5 deletions(-)
diff --git a/src/backend/utils/adt/uuid.c b/src/backend/utils/adt/uuid.c
index 246225a8735..3ecca6d3748 100644
--- a/src/backend/utils/adt/uuid.c
+++ b/src/backend/utils/adt/uuid.c
@@ -19,7 +19,9 @@
#include "common/hashfn.h"
#include "lib/hyperloglog.h"
#include "libpq/pqformat.h"
+#include "nodes/miscnodes.h"
#include "port/pg_bswap.h"
+#include "utils/builtins.h"
#include "utils/fmgrprotos.h"
#include "utils/guc.h"
#include "utils/skipsupport.h"
@@ -139,13 +141,13 @@ uuid_out(PG_FUNCTION_ARGS)
}
/*
- * We allow UUIDs as a series of 32 hexadecimal digits with an optional dash
- * after each group of 4 hexadecimal digits, and optionally surrounded by {}.
- * (The canonical format 8x-4x-4x-4x-12x, where "nx" means n hexadecimal
- * digits, is the only one used for output.)
+ * Reference implementation of the UUID grammar, parsing one character at a
+ * time. string_to_uuid() recognizes the common shapes more cheaply and
+ * defers to this function for everything else, so this is also the only
+ * place that reports a syntax error.
*/
static void
-string_to_uuid(const char *source, pg_uuid_t *uuid, Node *escontext)
+string_to_uuid_scalar(const char *source, pg_uuid_t *uuid, Node *escontext)
{
const char *src = source;
bool braces = false;
@@ -194,6 +196,88 @@ syntax_error:
"uuid", source)));
}
+/*
+ * We allow UUIDs as a series of 32 hexadecimal digits with an optional dash
+ * after each group of 4 hexadecimal digits, and optionally surrounded by {}.
+ * (The canonical format 8x-4x-4x-4x-12x, where "nx" means n hexadecimal
+ * digits, is the only one used for output.)
+ *
+ * The two common shapes -- a bare string of 32 hexadecimal digits and the
+ * canonical form, each optionally wrapped in braces -- are compacted into 32
+ * contiguous hex digits and decoded with hex_decode_safe(), which is much
+ * faster than the character-at-a-time loop. Any other shape, or any decoding
+ * error, is handed off to string_to_uuid_scalar() so that the accepted
+ * grammar and the error messages are unchanged.
+ *
+ * Note that this fast path is not conditional on SIMD support:
+ * hex_decode_safe() picks a vectorized or scalar implementation itself, and
+ * even its scalar implementation is far faster than string_to_uuid_scalar().
+ */
+static void
+string_to_uuid(const char *source, pg_uuid_t *uuid, Node *escontext)
+{
+ const char *body = source;
+ size_t len = strlen(source);
+ const char *hexsrc = NULL;
+ char hexbuf[32];
+ uint64 written;
+ ErrorSaveContext esctx = {T_ErrorSaveContext};
+
+ /* Strip one optional surrounding brace pair */
+ if (len >= 2 && source[0] == '{' && source[len - 1] == '}')
+ {
+ body = source + 1;
+ len -= 2;
+ }
+
+ if (len == 32)
+ {
+ /*
+ * Body is already 32 contiguous hex digits -- decode straight from
+ * the input. hex_decode_safe() reads exactly body[0..31], so it never
+ * touches the trailing NULL or '}'.
+ */
+ hexsrc = body;
+ }
+ else if (len == 36 && body[8] == '-' && body[13] == '-' &&
+ body[18] == '-' && body[23] == '-')
+ {
+ /*
+ * Canonical 8x-4x-4x-4x-12x form; compact them into hexbuf with
+ * fixed-offset copies, dropping the dashes.
+ */
+ memcpy(&hexbuf[0], &body[0], 8);
+ memcpy(&hexbuf[8], &body[9], 4);
+ memcpy(&hexbuf[12], &body[14], 4);
+ memcpy(&hexbuf[16], &body[19], 4);
+ memcpy(&hexbuf[20], &body[24], 12);
+ hexsrc = hexbuf;
+ }
+
+ if (hexsrc == NULL)
+ {
+ /* Uncommon shape; let the general parse handle it */
+ string_to_uuid_scalar(source, uuid, escontext);
+ return;
+ }
+
+ /*
+ * Decode the UUID hex data using our hex decoder that is SIMD-aware. We
+ * give it a private error context so that a decode failure is swallowed
+ * here and reported by the scalar path instead, keeping the error message
+ * identical.
+ */
+ written = hex_decode_safe(hexsrc, 32, (char *) uuid->data, (Node *) &esctx);
+
+ /*
+ * Fall back to the scalar path on any error. We must also reject a short
+ * result: hex_decode_safe() skips whitespace, so it can succeed yet write
+ * fewer than UUID_LEN bytes, whereas the UUID grammar forbids whitespace.
+ */
+ if (esctx.error_occurred || written != UUID_LEN)
+ string_to_uuid_scalar(source, uuid, escontext);
+}
+
Datum
uuid_recv(PG_FUNCTION_ARGS)
{
diff --git a/src/test/regress/expected/uuid.out b/src/test/regress/expected/uuid.out
index d542eb14b26..b40f50ac8be 100644
--- a/src/test/regress/expected/uuid.out
+++ b/src/test/regress/expected/uuid.out
@@ -375,5 +375,60 @@ SELECT v = v::bytea::uuid as matched FROM gen_random_uuid() v;
t
(1 row)
+-- Test UUID shapes that the parser uses the SIMD path.
+SELECT '5b35380a-7143-4912-9b55-f322699c6770'::uuid;
+ uuid
+--------------------------------------
+ 5b35380a-7143-4912-9b55-f322699c6770
+(1 row)
+
+SELECT '{5b35380a-7143-4912-9b55-f322699c6770}'::uuid;
+ uuid
+--------------------------------------
+ 5b35380a-7143-4912-9b55-f322699c6770
+(1 row)
+
+SELECT '5b35380a714349129b55f322699c6770'::uuid;
+ uuid
+--------------------------------------
+ 5b35380a-7143-4912-9b55-f322699c6770
+(1 row)
+
+SELECT '{5b35380a714349129b55f322699c6770}'::uuid;
+ uuid
+--------------------------------------
+ 5b35380a-7143-4912-9b55-f322699c6770
+(1 row)
+
+-- Test if the UUID parser using SIMD optimization correctly rejects invalid UUID
+-- string format.
+SELECT '5b35380a714349129b55f32 99c6770'::uuid;
+ERROR: invalid input syntax for type uuid: "5b35380a714349129b55f32 99c6770"
+LINE 1: SELECT '5b35380a714349129b55f32 99c6770'::uuid;
+ ^
+SELECT '5b35380a-7143-4912-9b55-f322699c67 '::uuid;
+ERROR: invalid input syntax for type uuid: "5b35380a-7143-4912-9b55-f322699c67 "
+LINE 1: SELECT '5b35380a-7143-4912-9b55-f322699c67 '::uuid;
+ ^
+SELECT ' 35380a-7143-4912-9b55-f322699c6770'::uuid;
+ERROR: invalid input syntax for type uuid: " 35380a-7143-4912-9b55-f322699c6770"
+LINE 1: SELECT ' 35380a-7143-4912-9b55-f322699c6770'::uuid;
+ ^
+SELECT 'AZ35380a-7143-4912-9b55-f322699c6770'::uuid;
+ERROR: invalid input syntax for type uuid: "AZ35380a-7143-4912-9b55-f322699c6770"
+LINE 1: SELECT 'AZ35380a-7143-4912-9b55-f322699c6770'::uuid;
+ ^
+SELECT '{AZ35380a-7143-4912-9b55-f322699c6770}'::uuid;
+ERROR: invalid input syntax for type uuid: "{AZ35380a-7143-4912-9b55-f322699c6770}"
+LINE 1: SELECT '{AZ35380a-7143-4912-9b55-f322699c6770}'::uuid;
+ ^
+SELECT '{AZ35380a714349129b55f322699c6770}'::uuid;
+ERROR: invalid input syntax for type uuid: "{AZ35380a714349129b55f322699c6770}"
+LINE 1: SELECT '{AZ35380a714349129b55f322699c6770}'::uuid;
+ ^
+SELECT '{AZ35380a714349129b55f322699c67 }'::uuid;
+ERROR: invalid input syntax for type uuid: "{AZ35380a714349129b55f322699c67 }"
+LINE 1: SELECT '{AZ35380a714349129b55f322699c67 }'::uuid;
+ ^
-- clean up
DROP TABLE guid1, guid2, guid3 CASCADE;
diff --git a/src/test/regress/sql/uuid.sql b/src/test/regress/sql/uuid.sql
index 54f0d8f8255..bdbeb91fe4c 100644
--- a/src/test/regress/sql/uuid.sql
+++ b/src/test/regress/sql/uuid.sql
@@ -178,5 +178,21 @@ SELECT '\x019a2f859ced7225b99d9c55044a2563'::bytea::uuid;
SELECT '\x1234567890abcdef'::bytea::uuid; -- error
SELECT v = v::bytea::uuid as matched FROM gen_random_uuid() v;
+-- Test UUID shapes that the parser uses the SIMD path.
+SELECT '5b35380a-7143-4912-9b55-f322699c6770'::uuid;
+SELECT '{5b35380a-7143-4912-9b55-f322699c6770}'::uuid;
+SELECT '5b35380a714349129b55f322699c6770'::uuid;
+SELECT '{5b35380a714349129b55f322699c6770}'::uuid;
+
+-- Test if the UUID parser using SIMD optimization correctly rejects invalid UUID
+-- string format.
+SELECT '5b35380a714349129b55f32 99c6770'::uuid;
+SELECT '5b35380a-7143-4912-9b55-f322699c67 '::uuid;
+SELECT ' 35380a-7143-4912-9b55-f322699c6770'::uuid;
+SELECT 'AZ35380a-7143-4912-9b55-f322699c6770'::uuid;
+SELECT '{AZ35380a-7143-4912-9b55-f322699c6770}'::uuid;
+SELECT '{AZ35380a714349129b55f322699c6770}'::uuid;
+SELECT '{AZ35380a714349129b55f322699c67 }'::uuid;
+
-- clean up
DROP TABLE guid1, guid2, guid3 CASCADE;
--
2.55.0
From dc286d4772945d1141a19f447c889116042d5b47 Mon Sep 17 00:00:00 2001
From: Masahiko Sawada <[email protected]>
Date: Wed, 5 Aug 2026 14:21:24 -0700
Subject: [PATCH v4 2/2] uuid_parse_bench module.
---
src/test/modules/uuid_parse_bench/Makefile | 23 +
src/test/modules/uuid_parse_bench/meson.build | 33 +
.../uuid_parse_bench--1.0.sql | 24 +
.../uuid_parse_bench/uuid_parse_bench.c | 641 ++++++++++++++++++
.../uuid_parse_bench/uuid_parse_bench.control | 5 +
5 files changed, 726 insertions(+)
create mode 100644 src/test/modules/uuid_parse_bench/Makefile
create mode 100644 src/test/modules/uuid_parse_bench/meson.build
create mode 100644 src/test/modules/uuid_parse_bench/uuid_parse_bench--1.0.sql
create mode 100644 src/test/modules/uuid_parse_bench/uuid_parse_bench.c
create mode 100644 src/test/modules/uuid_parse_bench/uuid_parse_bench.control
diff --git a/src/test/modules/uuid_parse_bench/Makefile b/src/test/modules/uuid_parse_bench/Makefile
new file mode 100644
index 00000000000..9919d7c8309
--- /dev/null
+++ b/src/test/modules/uuid_parse_bench/Makefile
@@ -0,0 +1,23 @@
+# src/test/modules/uuid_parse_bench/Makefile
+
+MODULE_big = uuid_parse_bench
+OBJS = \
+ $(WIN32RES) \
+ uuid_parse_bench.o
+PGFILEDESC = "uuid_parse_bench - microbenchmark for UUID input parsing"
+
+EXTENSION = uuid_parse_bench
+DATA = uuid_parse_bench--1.0.sql
+
+REGRESS = uuid_parse_bench
+
+ifdef USE_PGXS
+PG_CONFIG = pg_config
+PGXS := $(shell $(PG_CONFIG) --pgxs)
+include $(PGXS)
+else
+subdir = src/test/modules/uuid_parse_bench
+top_builddir = ../../../..
+include $(top_builddir)/src/Makefile.global
+include $(top_srcdir)/contrib/contrib-global.mk
+endif
diff --git a/src/test/modules/uuid_parse_bench/meson.build b/src/test/modules/uuid_parse_bench/meson.build
new file mode 100644
index 00000000000..8633ccd56ba
--- /dev/null
+++ b/src/test/modules/uuid_parse_bench/meson.build
@@ -0,0 +1,33 @@
+# Copyright (c) 2022-2026, PostgreSQL Global Development Group
+
+uuid_parse_bench_sources = files(
+ 'uuid_parse_bench.c',
+)
+
+if host_system == 'windows'
+ uuid_parse_bench_sources += rc_lib_gen.process(win32ver_rc, extra_args: [
+ '--NAME', 'uuid_parse_bench',
+ '--FILEDESC', 'uuid_parse_bench - microbenchmark for UUID input parsing',])
+endif
+
+uuid_parse_bench = shared_module('uuid_parse_bench',
+ uuid_parse_bench_sources,
+ kwargs: pg_test_mod_args,
+)
+test_install_libs += uuid_parse_bench
+
+test_install_data += files(
+ 'uuid_parse_bench.control',
+ 'uuid_parse_bench--1.0.sql',
+)
+
+tests += {
+ 'name': 'uuid_parse_bench',
+ 'sd': meson.current_source_dir(),
+ 'bd': meson.current_build_dir(),
+ 'regress': {
+ 'sql': [
+ 'uuid_parse_bench',
+ ],
+ },
+}
diff --git a/src/test/modules/uuid_parse_bench/uuid_parse_bench--1.0.sql b/src/test/modules/uuid_parse_bench/uuid_parse_bench--1.0.sql
new file mode 100644
index 00000000000..d49371fa7c5
--- /dev/null
+++ b/src/test/modules/uuid_parse_bench/uuid_parse_bench--1.0.sql
@@ -0,0 +1,24 @@
+/* src/test/modules/uuid_parse_bench/uuid_parse_bench--1.0.sql */
+
+-- complain if script is sourced in psql, rather than via CREATE EXTENSION
+\echo Use "CREATE EXTENSION uuid_parse_bench" to load this file. \quit
+
+--
+-- Time each parsing strategy against each input shape. Passing NULL for
+-- paths or shapes selects all of them.
+--
+-- Valid paths: scalar, simd, nosimd
+-- Valid shapes: canonical, bare32, braced_canonical, braced_bare32,
+-- dashed4, invalid_hex
+--
+CREATE FUNCTION uuid_parse_bench(nuuids int DEFAULT 100000,
+ nloops int DEFAULT 5,
+ paths text[] DEFAULT NULL,
+ shapes text[] DEFAULT NULL,
+ OUT path text,
+ OUT shape text,
+ OUT n_inputs int,
+ OUT best_ms float8,
+ OUT ns_per_parse float8)
+ RETURNS SETOF record
+ AS 'MODULE_PATHNAME' LANGUAGE C VOLATILE;
diff --git a/src/test/modules/uuid_parse_bench/uuid_parse_bench.c b/src/test/modules/uuid_parse_bench/uuid_parse_bench.c
new file mode 100644
index 00000000000..51e497fe98a
--- /dev/null
+++ b/src/test/modules/uuid_parse_bench/uuid_parse_bench.c
@@ -0,0 +1,641 @@
+/*--------------------------------------------------------------------------
+ *
+ * uuid_parse_bench.c
+ * Microbenchmark for the UUID input parsing strategies.
+ *
+ * This module times several ways of turning UUID text into a pg_uuid_t, so
+ * that the trade-offs can be argued with measurements rather than intuition.
+ * The paths it measures are:
+ *
+ * scalar the character-at-a-time loop using isxdigit() and strtoul()
+ * simd shape detection + compaction + hex_decode_safe()
+ * nosimd the same, but forced through the scalar hex decoder
+ *
+ * "simd" and "nosimd" differ only in which hex decoder they call, which is
+ * why this module carries its own copy of the scalar decoder: the real
+ * hex_decode_safe() picks its implementation at compile time, so a single
+ * backend binary can otherwise only measure one of the two.
+ *
+ * Copyright (c) 2007-2026, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * src/test/modules/uuid_parse_bench/uuid_parse_bench.c
+ *
+ * -------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include <ctype.h>
+
+#include "catalog/pg_type_d.h"
+#include "common/pg_prng.h"
+#include "fmgr.h"
+#include "funcapi.h"
+#include "lib/stringinfo.h"
+#include "miscadmin.h"
+#include "nodes/miscnodes.h"
+#include "portability/instr_time.h"
+#include "utils/array.h"
+#include "utils/builtins.h"
+#include "utils/tuplestore.h"
+#include "utils/uuid.h"
+
+PG_MODULE_MAGIC;
+
+PG_FUNCTION_INFO_V1(uuid_parse_bench);
+
+/* number of hex digits in a UUID body */
+#define UUID_HEX_LEN (UUID_LEN * 2)
+/* length of the canonical 8x-4x-4x-4x-12x form */
+#define UUID_CANON_LEN (UUID_HEX_LEN + 4)
+
+/*
+ * The parsing strategies we time. Keep in sync with path_names[].
+ */
+typedef enum BenchPath
+{
+ PATH_SCALAR = 0,
+ PATH_SIMD,
+ PATH_NOSIMD,
+ NUM_PATHS,
+} BenchPath;
+
+static const char *const path_names[] = {
+ "scalar", "simd", "nosimd",
+};
+
+/*
+ * The input shapes we time. The fast path's advantage depends strongly on
+ * the shape, so this has to be an axis of the measurement rather than a
+ * single representative case. dashed4 and invalid_hex are the shapes that
+ * fall back to the scalar parser, and so measure the cost of the fallback.
+ */
+typedef enum BenchShape
+{
+ SHAPE_CANONICAL = 0, /* 8x-4x-4x-4x-12x, what uuid_out() emits */
+ SHAPE_BARE32, /* 32 contiguous hex digits */
+ SHAPE_BRACED_CANONICAL, /* {8x-4x-4x-4x-12x} */
+ SHAPE_BRACED_BARE32, /* {32 hex digits} */
+ SHAPE_DASHED4, /* dash after every group of 4: falls back */
+ SHAPE_INVALID_HEX, /* canonical but with a bad digit: falls back */
+ NUM_SHAPES,
+} BenchShape;
+
+static const char *const shape_names[] = {
+ "canonical", "bare32", "braced_canonical", "braced_bare32",
+ "dashed4", "invalid_hex",
+};
+
+/*
+ * Consume parse results so the compiler cannot delete the work we are timing.
+ */
+static volatile uint64 bench_sink = 0;
+
+/*
+ * Copy of string_to_uuid_scalar() from src/backend/utils/adt/uuid.c.
+ */
+static bool
+string_to_uuid_scalar(const char *source, pg_uuid_t *uuid)
+{
+ const char *src = source;
+ bool braces = false;
+ int i;
+
+ if (src[0] == '{')
+ {
+ src++;
+ braces = true;
+ }
+
+ for (i = 0; i < UUID_LEN; i++)
+ {
+ char str_buf[3];
+
+ if (src[0] == '\0' || src[1] == '\0')
+ return false;
+ memcpy(str_buf, src, 2);
+ if (!isxdigit((unsigned char) str_buf[0]) ||
+ !isxdigit((unsigned char) str_buf[1]))
+ return false;
+
+ str_buf[2] = '\0';
+ uuid->data[i] = (unsigned char) strtoul(str_buf, NULL, 16);
+ src += 2;
+ if (src[0] == '-' && (i % 2) == 1 && i < UUID_LEN - 1)
+ src++;
+ }
+
+ if (braces)
+ {
+ if (*src != '}')
+ return false;
+ src++;
+ }
+
+ return *src == '\0';
+}
+
+/*
+ * hexlookup[], get_hex() and hex_decode_safe_scalar() below are copies of the
+ * versions in src/backend/utils/adt/encode.c. They are static there, and in
+ * any case the exported hex_decode_safe() resolves to either the vectorized
+ * or the scalar implementation at compile time, so a backend built for a SIMD
+ * platform cannot otherwise measure what a non-SIMD platform would do.
+ *
+ * Being copies, these can drift from the originals. Re-check them against
+ * encode.c before quoting any figure.
+ */
+static const int8 bench_hexlookup[128] = {
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, -1, -1, -1, -1, -1, -1,
+ -1, 10, 11, 12, 13, 14, 15, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, 10, 11, 12, 13, 14, 15, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+};
+
+static inline bool
+bench_get_hex(const char *cp, char *out)
+{
+ unsigned char c = (unsigned char) *cp;
+ int res = -1;
+
+ if (c < 127)
+ res = bench_hexlookup[c];
+
+ *out = (char) res;
+
+ return (res >= 0);
+}
+
+/*
+ * Copy of hex_decode_safe_scalar() in encode.c.
+ */
+static inline uint64
+bench_hex_decode_scalar(const char *src, size_t len, char *dst, bool *ok)
+{
+ const char *s,
+ *srcend;
+ char v1,
+ v2,
+ *p;
+
+ srcend = src + len;
+ s = src;
+ p = dst;
+ *ok = true;
+ while (s < srcend)
+ {
+ if (*s == ' ' || *s == '\n' || *s == '\t' || *s == '\r')
+ {
+ s++;
+ continue;
+ }
+ if (!bench_get_hex(s, &v1))
+ {
+ *ok = false;
+ return p - dst;
+ }
+ s++;
+ if (s >= srcend)
+ {
+ *ok = false;
+ return p - dst;
+ }
+ if (!bench_get_hex(s, &v2))
+ {
+ *ok = false;
+ return p - dst;
+ }
+ s++;
+ *p++ = (v1 << 4) | v2;
+ }
+
+ return p - dst;
+}
+
+/* ----------------------------------------------------------------
+ * Input classification, shared by the fast paths
+ * ----------------------------------------------------------------
+ */
+
+/*
+ * How a given input can be decoded. Note these are not BenchShape values:
+ * they say which fast path applies, not which shape the corpus was built in.
+ */
+#define FASTPATH_NONE 0 /* no fast path applies; hand off to scalar */
+#define FASTPATH_CONTIGUOUS 1 /* 32 hex digits, decode in place */
+#define FASTPATH_COMPACT 2 /* canonical, must have its dashes removed */
+
+/*
+ * Decide which fast path applies to source and, when one does, hand back the
+ * start of the body. This is exactly the work string_to_uuid() does before
+ * it decodes anything.
+ */
+static inline int
+classify_input(const char *source, const char **bodyp)
+{
+ const char *body = source;
+ size_t len = strlen(source);
+
+ /* Strip one optional surrounding brace pair */
+ if (len >= 2 && source[0] == '{' && source[len - 1] == '}')
+ {
+ body = source + 1;
+ len -= 2;
+ }
+
+ *bodyp = body;
+
+ if (len == UUID_HEX_LEN)
+ return FASTPATH_CONTIGUOUS;
+ if (len == UUID_CANON_LEN && body[8] == '-' && body[13] == '-' &&
+ body[18] == '-' && body[23] == '-')
+ return FASTPATH_COMPACT;
+
+ return FASTPATH_NONE;
+}
+
+/*
+ * Compact the canonical form into 32 contiguous hex digits.
+ */
+static inline void
+compact_canonical(const char *body, char *hexbuf)
+{
+ memcpy(&hexbuf[0], &body[0], 8);
+ memcpy(&hexbuf[8], &body[9], 4);
+ memcpy(&hexbuf[12], &body[14], 4);
+ memcpy(&hexbuf[16], &body[19], 4);
+ memcpy(&hexbuf[20], &body[24], 12);
+}
+
+/*
+ * Copy of hex_decode_safe() with SIMD
+ */
+static bool
+hex_decode_safe_simd(const char *source, pg_uuid_t *uuid)
+{
+ const char *body;
+ const char *hexsrc;
+ char hexbuf[UUID_HEX_LEN];
+ uint64 written;
+ ErrorSaveContext esctx = {T_ErrorSaveContext};
+ int fastpath = classify_input(source, &body);
+
+ if (fastpath == FASTPATH_NONE)
+ return string_to_uuid_scalar(source, uuid);
+
+ if (fastpath == FASTPATH_COMPACT)
+ {
+ compact_canonical(body, hexbuf);
+ hexsrc = hexbuf;
+ }
+ else
+ hexsrc = body;
+
+ written = hex_decode_safe(hexsrc, UUID_HEX_LEN, (char *) uuid->data,
+ (Node *) &esctx);
+
+ if (esctx.error_occurred || written != UUID_LEN)
+ return string_to_uuid_scalar(source, uuid);
+
+ return true;
+}
+
+/*
+ * Simulating hex_decode_safe() with USE_NO_SIMD
+ */
+static bool
+hex_decode_safe_nosimd(const char *source, pg_uuid_t *uuid)
+{
+ const char *body;
+ const char *hexsrc;
+ char hexbuf[UUID_HEX_LEN];
+ uint64 written;
+ bool ok;
+ int fastpath = classify_input(source, &body);
+
+ if (fastpath == FASTPATH_NONE)
+ return string_to_uuid_scalar(source, uuid);
+
+ if (fastpath == FASTPATH_COMPACT)
+ {
+ compact_canonical(body, hexbuf);
+ hexsrc = hexbuf;
+ }
+ else
+ hexsrc = body;
+
+ written = bench_hex_decode_scalar(hexsrc, UUID_HEX_LEN,
+ (char *) uuid->data, &ok);
+
+ if (!ok || written != UUID_LEN)
+ return string_to_uuid_scalar(source, uuid);
+
+ return true;
+}
+
+/*
+ * Render one random UUID in the requested shape. The caller owns the buffer,
+ * which must hold at least 48 bytes.
+ */
+static void
+render_shape(pg_prng_state *state, BenchShape shape, char *buf)
+{
+ unsigned char b[UUID_LEN];
+ static const char hex[] = "0123456789abcdef";
+ char hexdigits[UUID_HEX_LEN];
+ char *p = buf;
+ int i;
+
+ for (i = 0; i < UUID_LEN; i++)
+ b[i] = (unsigned char) pg_prng_uint32(state);
+ for (i = 0; i < UUID_LEN; i++)
+ {
+ hexdigits[i * 2] = hex[b[i] >> 4];
+ hexdigits[i * 2 + 1] = hex[b[i] & 0x0f];
+ }
+
+ if (shape == SHAPE_BRACED_CANONICAL || shape == SHAPE_BRACED_BARE32)
+ *p++ = '{';
+
+ switch (shape)
+ {
+ case SHAPE_BARE32:
+ case SHAPE_BRACED_BARE32:
+ memcpy(p, hexdigits, UUID_HEX_LEN);
+ p += UUID_HEX_LEN;
+ break;
+
+ case SHAPE_DASHED4:
+ /* a dash after every group of 4: legal, but not the fast path */
+ for (i = 0; i < UUID_HEX_LEN; i++)
+ {
+ if (i > 0 && i % 4 == 0)
+ *p++ = '-';
+ *p++ = hexdigits[i];
+ }
+ break;
+
+ default:
+ /* canonical 8x-4x-4x-4x-12x, optionally with a bad digit */
+ for (i = 0; i < UUID_HEX_LEN; i++)
+ {
+ if (i == 8 || i == 12 || i == 16 || i == 20)
+ *p++ = '-';
+ *p++ = hexdigits[i];
+ }
+ /* corrupt one digit so the fast path has to hand off */
+ if (shape == SHAPE_INVALID_HEX)
+ buf[1] = 'z';
+ break;
+ }
+
+ if (shape == SHAPE_BRACED_CANONICAL || shape == SHAPE_BRACED_BARE32)
+ *p++ = '}';
+ *p = '\0';
+}
+
+/*
+ * Build nuuids input strings of the given shape in the current memory
+ * context. Everything is materialized before timing starts so that neither
+ * the PRNG nor the allocator shows up in the measurement.
+ */
+static char **
+build_corpus(BenchShape shape, int nuuids, uint64 seed)
+{
+ pg_prng_state state;
+ char **inputs = palloc(sizeof(char *) * nuuids);
+ int i;
+
+ pg_prng_seed(&state, seed);
+ for (i = 0; i < nuuids; i++)
+ {
+ char buf[48];
+
+ render_shape(&state, shape, buf);
+ inputs[i] = pstrdup(buf);
+ }
+
+ return inputs;
+}
+
+/* ----------------------------------------------------------------
+ * Timing
+ * ----------------------------------------------------------------
+ */
+
+/*
+ * Run one path over one corpus nloops times and return the best wall time in
+ * milliseconds. The best of several runs is reported rather than the mean
+ * because it is the least contaminated by scheduling noise.
+ */
+/*
+ * The inner loop, instantiated once per path. Calling the parser directly
+ * rather than through parse_funcs[] matters: an indirect call would add the
+ * same few cycles to every path and so shrink exactly the differences this
+ * module exists to measure. uuid_in() calls string_to_uuid() directly, so
+ * direct calls are also the more faithful model.
+ */
+#define TIME_ONE_LOOP(fn) \
+ do { \
+ for (i = 0; i < nuuids; i++) \
+ { \
+ pg_uuid_t u; \
+ if (fn(inputs[i], &u)) \
+ local += u.data[0]; \
+ else \
+ local++; \
+ } \
+ } while (0)
+
+static double
+time_path(BenchPath path, char **inputs, int nuuids, int nloops)
+{
+ double best = -1.0;
+ int loop;
+
+ for (loop = 0; loop < nloops; loop++)
+ {
+ instr_time start,
+ stop;
+ uint64 local = 0;
+ double ms;
+ int i;
+
+ CHECK_FOR_INTERRUPTS();
+
+ INSTR_TIME_SET_CURRENT(start);
+ switch (path)
+ {
+ case PATH_SCALAR:
+ TIME_ONE_LOOP(string_to_uuid_scalar);
+ break;
+ case PATH_SIMD:
+ TIME_ONE_LOOP(hex_decode_safe_simd);
+ break;
+ case PATH_NOSIMD:
+ TIME_ONE_LOOP(hex_decode_safe_nosimd);
+ break;
+ case NUM_PATHS:
+ elog(ERROR, "unexpected path %d", (int) path);
+ break;
+ }
+ INSTR_TIME_SET_CURRENT(stop);
+ INSTR_TIME_SUBTRACT(stop, start);
+
+ bench_sink += local;
+
+ ms = INSTR_TIME_GET_DOUBLE(stop) * 1000.0;
+ if (best < 0.0 || ms < best)
+ best = ms;
+ }
+
+ return best;
+}
+
+/* ----------------------------------------------------------------
+ * SQL interface
+ * ----------------------------------------------------------------
+ */
+
+/*
+ * Turn a text[] filter into a bool[] over names, or select everything when
+ * the array is NULL.
+ */
+static void
+parse_filter(ArrayType *arr, const char *const *names, int nnames,
+ bool *selected, const char *what)
+{
+ Datum *elems;
+ bool *nulls;
+ int nelems;
+ int i;
+
+ if (arr == NULL)
+ {
+ for (i = 0; i < nnames; i++)
+ selected[i] = true;
+ return;
+ }
+
+ for (i = 0; i < nnames; i++)
+ selected[i] = false;
+
+ deconstruct_array_builtin(arr, TEXTOID, &elems, &nulls, &nelems);
+ for (i = 0; i < nelems; i++)
+ {
+ char *name;
+ int j;
+
+ if (nulls[i])
+ ereport(ERROR,
+ (errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED),
+ errmsg("%s array must not contain nulls", what)));
+
+ name = TextDatumGetCString(elems[i]);
+ for (j = 0; j < nnames; j++)
+ {
+ if (strcmp(name, names[j]) == 0)
+ {
+ selected[j] = true;
+ break;
+ }
+ }
+ if (j == nnames)
+ {
+ StringInfoData buf;
+
+ initStringInfo(&buf);
+ for (j = 0; j < nnames; j++)
+ appendStringInfo(&buf, "%s%s", j > 0 ? ", " : "", names[j]);
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("unrecognized %s \"%s\"", what, name),
+ errhint("Valid values are: %s.", buf.data)));
+ }
+ }
+}
+
+/*
+ * uuid_parse_bench(nuuids, nloops, paths, shapes)
+ *
+ * Time every selected path against every selected shape and return the
+ * matrix.
+ */
+Datum
+uuid_parse_bench(PG_FUNCTION_ARGS)
+{
+ int nuuids = PG_GETARG_INT32(0);
+ int nloops = PG_GETARG_INT32(1);
+ ArrayType *patharr = PG_ARGISNULL(2) ? NULL : PG_GETARG_ARRAYTYPE_P(2);
+ ArrayType *shapearr = PG_ARGISNULL(3) ? NULL : PG_GETARG_ARRAYTYPE_P(3);
+ bool want_path[NUM_PATHS];
+ bool want_shape[NUM_SHAPES];
+ ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
+ int shape;
+
+ if (nuuids <= 0)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("nuuids must be positive")));
+ if (nloops <= 0)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("nloops must be positive")));
+
+ parse_filter(patharr, path_names, NUM_PATHS, want_path, "path");
+ parse_filter(shapearr, shape_names, NUM_SHAPES, want_shape, "shape");
+
+ InitMaterializedSRF(fcinfo, 0);
+
+ for (shape = 0; shape < NUM_SHAPES; shape++)
+ {
+ MemoryContext corpusctx,
+ oldctx;
+ char **inputs;
+ int path;
+
+ if (!want_shape[shape])
+ continue;
+
+ /*
+ * Build each corpus in its own context so that the strings for one
+ * shape are freed before the next is built. With a large nuuids the
+ * whole matrix would otherwise be resident at once.
+ */
+ corpusctx = AllocSetContextCreate(CurrentMemoryContext,
+ "uuid_parse_bench corpus",
+ ALLOCSET_DEFAULT_SIZES);
+ oldctx = MemoryContextSwitchTo(corpusctx);
+ inputs = build_corpus(shape, nuuids, 42);
+ MemoryContextSwitchTo(oldctx);
+
+ for (path = 0; path < NUM_PATHS; path++)
+ {
+ Datum values[5];
+ bool nulls[5] = {0};
+ double best_ms;
+
+ if (!want_path[path])
+ continue;
+
+ best_ms = time_path(path, inputs, nuuids, nloops);
+
+ values[0] = CStringGetTextDatum(path_names[path]);
+ values[1] = CStringGetTextDatum(shape_names[shape]);
+ values[2] = Int32GetDatum(nuuids);
+ values[3] = Float8GetDatum(best_ms);
+ values[4] = Float8GetDatum(best_ms * 1000000.0 / nuuids);
+
+ tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc,
+ values, nulls);
+ }
+
+ MemoryContextDelete(corpusctx);
+ }
+
+ return (Datum) 0;
+}
diff --git a/src/test/modules/uuid_parse_bench/uuid_parse_bench.control b/src/test/modules/uuid_parse_bench/uuid_parse_bench.control
new file mode 100644
index 00000000000..60e75d3df2d
--- /dev/null
+++ b/src/test/modules/uuid_parse_bench/uuid_parse_bench.control
@@ -0,0 +1,5 @@
+# uuid_parse_bench extension
+comment = 'Microbenchmark for UUID input parsing'
+default_version = '1.0'
+module_pathname = '$libdir/uuid_parse_bench'
+relocatable = true
--
2.55.0