Re: Optimize UUID parse using SIMD

From: Bharath Rupireddy <bharath(dot)rupireddyforpostgres(at)gmail(dot)com>
To: Masahiko Sawada <sawada(dot)mshk(at)gmail(dot)com>
Cc: Haibo Yan <tristan(dot)yim(at)gmail(dot)com>, PostgreSQL-development <pgsql-hackers(at)postgresql(dot)org>
Subject: Re: Optimize UUID parse using SIMD
Date: 2026-08-06 21:42:29
Message-ID: CALj2ACW_XHVm+-a3HEnRU63ft9=9MdMDUno-s1EKxHVzp6pVNQ@mail.gmail.com
Views: Whole Thread | Raw Message | Download mbox | Resend email
Thread:
Lists: pgsql-hackers

Hi,

On Wed, Aug 5, 2026 at 5:20 PM Masahiko Sawada <sawada(dot)mshk(at)gmail(dot)com> wrote:
>
> 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.

In v3, uuid.c doesn't include port/simd.h, so USE_NO_SIMD isn't
defined there. That makes the USE_NO_SIMD check in string_to_uuid()
confusing, since it tests a macro that isn't visible in this file. The
check reads as though it sends no-SIMD builds to the scalar parser,
but because the macro is undefined, string_to_uuid() always runs the
fast path, even on a no-SIMD build. That does no harm because the fast
path still falls back to the scalar parser for uncommon shapes and
errors, and hex_decode_safe() picks its own scalar path when SIMD is
off, which is already faster than isxdigit()+strtoul(). So the gate
wasn't buying anything, and v4 drops it, which is the right call.

> 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.

I ran the benchmark locally and see similar numbers. Both paths in
hex_decode_safe() beat the current isxdigit()+strtoul() approach. The
SIMD path is roughly 20-40x faster on the common shapes, and even the
no-SIMD path is about 15x faster.

path | shape | n_inputs | best_ms
--------+------------------+----------+---------
scalar | canonical | 100000 | 31.820
simd | canonical | 100000 | 1.393
nosimd | canonical | 100000 | 2.136
scalar | bare32 | 100000 | 29.585
simd | bare32 | 100000 | 0.699
nosimd | bare32 | 100000 | 2.002
scalar | braced_canonical | 100000 | 31.906
simd | braced_canonical | 100000 | 1.412
nosimd | braced_canonical | 100000 | 2.143
scalar | braced_bare32 | 100000 | 29.170
simd | braced_bare32 | 100000 | 0.751
nosimd | braced_bare32 | 100000 | 2.031
scalar | dashed4 | 100000 | 30.424
simd | dashed4 | 100000 | 30.869
nosimd | dashed4 | 100000 | 30.900
scalar | invalid_hex | 100000 | 0.320
simd | invalid_hex | 100000 | 1.752
nosimd | invalid_hex | 100000 | 0.708
(18 rows)

The attached v4-0001 patch looks good to me. One comment though. Can
we add a positive test case that hits string_to_uuid_scalar and does
not error out?

--
Bharath Rupireddy
Amazon Web Services: https://aws.amazon.com

In response to

Browse pgsql-hackers by date

  From Date Subject
Next Message Daniel Gustafsson 2026-08-06 21:45:29 Re: [PATCH] Remove unused scram_client_key_len and scram_server_key_len fields
Previous Message Nathan Bossart 2026-08-06 21:38:28 Re: problems with toast.* reloptions