Re: Optimize UUID parse using SIMD

From: Masahiko Sawada <sawada(dot)mshk(at)gmail(dot)com>
To: Haibo Yan <tristan(dot)yim(at)gmail(dot)com>
Cc: PostgreSQL-development <pgsql-hackers(at)postgresql(dot)org>
Subject: Re: Optimize UUID parse using SIMD
Date: 2026-08-06 00:19:19
Message-ID: CAD21AoBVZTkg_E+Bam7Df6KgCur+rhBL9+hWZqR+YMsAoVgbxw@mail.gmail.com
Views: Whole Thread | Raw Message | Download mbox | Resend email
Thread:
Lists: pgsql-hackers

On Tue, Jun 30, 2026 at 11:03 AM Haibo Yan <tristan(dot)yim(at)gmail(dot)com> wrote:
>
> On Tue, Jun 30, 2026 at 10:53 AM Masahiko Sawada <sawada(dot)mshk(at)gmail(dot)com> wrote:
> >
> > On Mon, Jun 29, 2026 at 5:53 PM Haibo Yan <tristan(dot)yim(at)gmail(dot)com> wrote:
> > >
> > > On Mon, Jun 29, 2026 at 2:55 PM Masahiko Sawada <sawada(dot)mshk(at)gmail(dot)com> wrote:
> > > >
> > > > On Sun, Jun 28, 2026 at 7:20 PM Haibo Yan <tristan(dot)yim(at)gmail(dot)com> wrote:
> > > > >
> > > > > On Thu, Jun 25, 2026 at 3:16 PM Masahiko Sawada <sawada(dot)mshk(at)gmail(dot)com> wrote:
> > > > > >
> > > > > > On Thu, Jun 25, 2026 at 2:31 PM Haibo Yan <tristan(dot)yim(at)gmail(dot)com> wrote:
> > > > > > >
> > > > > > >
> > > > > > >
> > > > > > > On Thu, Jun 25, 2026 at 11:28 AM Masahiko Sawada <sawada(dot)mshk(at)gmail(dot)com> 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

Attachment Content-Type Size
v4-0001-Optimize-UUID-parse-using-SIMD.patch text/x-patch 10.0 KB
v4-0002-uuid_parse_bench-module.patch text/x-patch 20.6 KB

In response to

Responses

Browse pgsql-hackers by date

  From Date Subject
Next Message Chao Li 2026-08-06 00:29:24 Re: Make printTableAddCell/printTableAddHeader string argument const
Previous Message Michael Paquier 2026-08-05 23:38:48 Re: Fix archive restore race that could unlink WAL before rename