Skip to content

gh-139122: Reimplement base UUID type, uuid4(), and uuid7() in C - #139123

Open
1st1 wants to merge 28 commits into
python:mainfrom
1st1:uuid_push
Open

1st1 wants to merge 28 commits into
python:mainfrom
1st1:uuid_push

Conversation

@1st1

@1st1 1st1 commented Sep 18, 2025 •

Copy link
Copy Markdown
Member

The C implementation considerably boosts the performance of the key UUID operations:

------------------------------------
Operation                    Speedup
------------------------------------
uuid4() generation            15.01x
uuid7() generation            29.64x
UUID from string               6.76x
UUID from bytes                5.16x
str(uuid) conversion           6.66x
------------------------------------

Summary of changes:

  • The UUID type is reimplemented in C in its entirety.

  • The pure-Python is kept around and is used of the C implementation isn't available for some reason.

  • Both implementations are tested extensively; additional tests are added to ensure that the C implementation of the type follows the pure Python implementation fully.

  • The Python implementation stores UUID values as int objects. The C implementation stores them as uint8_t[16] array.

  • The C implementation supports unpickling of UUIDs created with Python 2 using protocols starting with 0.

  • The C implementation has faster hash() implementation but also caches the computed hash value to speedup cases when UUIDs are used as set/dict keys.

  • The C implementation has a freelist to make new UUID object instantiation as fast as possible.

  • uuid4() and uuid7() are now implmented in C. The most performance boost (10x) comes from overfetching entropy to decrease the number of _PyOS_URandom() calls. On its own it's a safe optimization with the edge case that Unix fork needs to be explicitly handled. We do that by comparing the current PID to the PID of when the random buffer was populated.

  • Portions of code are coming from my implementation of faster UUID in gel-python [1]. I did use AI during the development, but basically had to rewrite the code it generated to be more idiomatic and efficient.

  • The benchmark can be found here [2].

  • This PR makes Python UUID operations as fast as they are in NodeJS and Bun runtimes.

[1] https://github.com/MagicStack/py-pgproto/blob/b8109fb311a59f30f9947567a13508da9a776564/uuid.pyx

[2] https://gist.github.com/1st1/f03e816f34a61e4d46c78ff98baf4818

@python-cla-bot

python-cla-bot Bot commented Sep 18, 2025 •

Copy link
Copy Markdown

All commit authors signed the Contributor License Agreement.

CLA signed

@1st1 1st1 changed the title The C implementation considerably boosts the performance of the key UUID Reimplement base UUID type, uuid4(), and uuid7() in C Sep 18, 2025
1st1 added a commit to 1st1/cpython that referenced this pull request Sep 18, 2025
… in C

The C implementation considerably boosts the performance of the key UUID
operations:

------------------------------------
Operation                    Speedup
------------------------------------
uuid4() generation            15.01x
uuid7() generation            29.64x
UUID from string               6.76x
UUID from bytes                5.16x
str(uuid) conversion           6.66x
------------------------------------

Summary of changes:

* The UUID type is reimplemented in C in its entirety.

* The pure-Python is kept around and is used of the C implementation
  isn't available for some reason.

* Both implementations are tested extensively; additional tests are
  added to ensure that the C implementation of the type follows the pure
  Python implementation fully.

* The Python implementation stores UUID values as int objects. The C
  implementation stores them as `uint8_t[16]` array.

* The C implementation supports unpickling of UUIDs created with Python
  2 using protocols starting with 0. That necessitated a small fix to
  the `copyreg` module (the change is only affecting legacy pickle
  pathway).

* The C implementation has faster hash() implementation but also caches
  the computed hash value to speedup cases when UUIDs are used as
  set/dict keys.

* The C implementation has a freelist to make new UUID object
  instantiation as fast as possible.

* uuid4() and uuid7() are now implmented in C. The most performance
  boost (10x) comes from overfetching entropy to decrease the number of
  _PyOS_URandom() calls. On its own it's a safe optimization with the
  edge case that Unix fork needs to be explicitly handled. We do that by
  comparing the current PID to the PID of when the random buffer was
  populated.

* Portions of code are coming from my implementation of faster UUID
  in gel-python [1]. I did use AI during the development, but basically
  had to rewrite the code it generated to be more idiomatic and
  efficient.

* The benchmark can be found here [2].

* This PR makes Python UUID operations as fast as they are in NodeJS and
  Bun runtimes.

[1] https://github.com/MagicStack/py-pgproto/blob/b8109fb311a59f30f9947567a13508da9a776564/uuid.pyx

[2] https://gist.github.com/1st1/f03e816f34a61e4d46c78ff98baf4818
@1st1
1st1 force-pushed the uuid_push branch 2 times, most recently from 2d0a381 to c4363f8 Compare September 18, 2025 13:22
@1st1 1st1 changed the title Reimplement base UUID type, uuid4(), and uuid7() in C #139122: Reimplement base UUID type, uuid4(), and uuid7() in C Sep 18, 2025
@1st1 1st1 changed the title #139122: Reimplement base UUID type, uuid4(), and uuid7() in C #gh-139122: Reimplement base UUID type, uuid4(), and uuid7() in C Sep 18, 2025
@1st1
1st1 force-pushed the uuid_push branch 2 times, most recently from fa12192 to 4a5c2a2 Compare September 18, 2025 13:24
@1st1

1st1 commented Sep 18, 2025

Copy link
Copy Markdown
Member Author

Note to reviewers:

  • Please compare uuid7 C and Python implementations side by side
  • Please review all of the bitshift/magic code closely (Python implementation uses int, i'm using char[16], so low-level operations are very different but have to be equivalent)

Comment thread Modules/_uuidmodule.c

*counter = (((uint64_t)(high & 0x1FF) << 32) | (low >> 32)) & 0x1FFFFFFFFFF;
*tail = (uint32_t)low;
return 0;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't tested thoroughly (mostly looking at GodBolt output), but this ought to generate more efficient code (and there might be more ways to generate the right values directly, I just didn't go that far):

    struct {
        uint16_t high;
        uint32_t low1;
        uint32_t low2;
    } rand_bytes;
    if (gen_random(state, (uint8_t*)&rand_bytes, sizeof(rand_bytes)) < 0) {
        return -1;
    }

    *counter = (((uint64_t)(rand_bytes.high & 0x1FF) << 32) | (rand_bytes.low1)) & 0x1FFFFFFFFFF;
    *tail = rand_bytes.low2;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@1st1 for visibility

Comment thread Modules/_uuidmodule.c Outdated
Comment thread Modules/_uuidmodule.c
Comment thread Modules/_uuidmodule.c Outdated
Comment thread Modules/_uuidmodule.c Outdated
@StanFromIreland StanFromIreland changed the title #gh-139122: Reimplement base UUID type, uuid4(), and uuid7() in C gh-139122: Reimplement base UUID type, uuid4(), and uuid7() in C Sep 18, 2025
@picnixz

picnixz commented Sep 18, 2025

Copy link
Copy Markdown
Member

I would be happy to have a C implementation of UUID but for reviewing purposes, may I suggest that we first focus on implementing the wrapper in C in one PR and have different PRs for each UUID? it would be much easier to focus on the algorithm to review.

@picnixz picnixz left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are the first round comments I have. I realy want multiple PRs because the module becomes really huge and there are parts that I don't think we need to reimplement in C.

Comment thread Lib/uuid.py
Comment thread Lib/uuid.py Outdated
Comment thread Modules/_uuidmodule.c
Comment thread Modules/_uuidmodule.c
Comment thread Modules/_uuidmodule.c
Comment thread Modules/_uuidmodule.c Outdated
Comment thread Modules/_uuidmodule.c Outdated
Comment thread Modules/_uuidmodule.c Outdated
Comment thread Modules/_uuidmodule.c Outdated
Comment thread Modules/_uuidmodule.c Outdated
@1st1

1st1 commented Sep 19, 2025

Copy link
Copy Markdown
Member Author

I realy want multiple PRs because the module becomes really huge and there are parts that I don't think we need to reimplement in C.

Sorry, I'm not going to do multiple PRs, I've no time for that. But I will address your comments here for sure!

@1st1
1st1 force-pushed the uuid_push branch 2 times, most recently from da233b4 to ba9217e Compare September 19, 2025 10:59
@picnixz

picnixz commented Sep 19, 2025

Copy link
Copy Markdown
Member

I would REALLY appreciate smaller PRs. Usually this is our (current) workflow, namely incrementing changes. I think other core devs would agree with me here (@vstinner, @serhiy-storchaka and @encukou).

Note that whatever we do, this is a feature that will only be included in 3.15 so I can also make the small PRs once I am back at home. Changing 3.14 is no more possible unless the RM gives their approval here (cc @hugovk, who could also whether he prefers one or multiple PR) as performance improvements are usually considered as new features (especially if we are adding a C implementation).

@1st1

1st1 commented Sep 19, 2025 •

Copy link
Copy Markdown
Member Author

I would REALLY appreciate smaller PRs. Usually this is our (current) workflow, namely incrementing changes. I think other core devs would agree with me here (@vstinner, @serhiy-storchaka and @encukou).

The current PR layout:

  1. 90% is reimplementing the UUID type
  2. 10% is uuid4 and uuid7 implementations which are isolated separate functions from (1); uuid4 is very tiny.

I'm usually also for making smaller PRs, but objectively there's no point for that right here; I don't see how reviewing this would be fundamentally any different if instead of one PR with 1700 lines in uuidmodule.c, you'd have 3 PRs, one with 1600 lines and another with like 100 lines, and yet another with 30. Reviewing time would be the same, no? What's the point?

If you absolutely insist I can do this, but tbqh I really don't understand why you're pushing for this so hard. It would create a lot of work for me and not necessarily make reviewers more happy.

Note that whatever we do, this is a feature that will only be included in 3.15 so I can also make the small PRs once I am back at home. Changing 3.14 is no more possible unless the RM gives their approval here (cc @hugovk, who could also whether he prefers one or multiple PR) as performance improvements are usually considered as new features (especially if we are adding a C implementation).

I don't think this is 3.14 material and I wasn't pushing for that. 3.15 is fine (unless other people but me want this to be in 3.14, in which case I won't say no).

That said I'd like to see my work through and would love to still merge this myself.

Comment thread Modules/_uuidmodule.c Outdated
@1st1

1st1 commented Sep 21, 2025

Copy link
Copy Markdown
Member Author

@picnixz I'm sorry for my snappiness (I blame 4 days spent deep in C coding! :)) I'm adding some basic machinery to test that C implementation of uuid() functions behave identically. Since this is even more extra code it's now very reasonable to split this into two prs: one for the base UUID type, another for uuid4() and uuid7(). I'll do that later.

@1st1
1st1 requested a review from FFY00 as a code owner October 19, 2026 22:35
@bedevere-app bedevere-app Bot added the type-feature A feature request or enhancement label Oct 19, 2026
@1st1
1st1 requested a review from AA-Turner as a code owner October 19, 2026 23:23
@read-the-docs-community

read-the-docs-community Bot commented Oct 19, 2026 •

Copy link
Copy Markdown

Comment thread Doc/whatsnew/3.16.rst
@eendebakpt

eendebakpt commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

The following reproducer crashes for me:

import gc
import sys
import uuid

_uuid = sys.modules['_uuid']
UUID = _uuid.UUID
assert uuid.UUID is UUID, "C implementation not in use"

# Pass 1: drop the Python module and its NAMESPACE_*/NIL/MAX instances, so no
# base-type instance is left to keep the C type alive.
del sys.modules['uuid']
del uuid
gc.collect()

class Sub(UUID):
    pass

Sub.keep = Sub(int=1)   # instance reachable only from the (younger) subclass

del sys.modules['_uuid']
del _uuid, UUID, Sub
print('final collect')
gc.collect()            # module, base type, Sub and Sub.keep are all garbage
print("survived")

The crash is not due to the freelist, but is related: the crash is due to module state lookup which is there for the freelist.

The _asyncio module also is an extension that uses the cpython freelist. So I would prefer to either keep the freelist out of the PR (and make a followup PR), or use the standard cpython freelist mechanism.

@1st1

1st1 commented Oct 20, 2026

Copy link
Copy Markdown
Member Author

@eendebakpt

The following reproducer crashes for me:

Should be fixed!

@1st1 1st1 removed the stale Stale PR or inactive for long period of time. label Oct 20, 2026

@picnixz picnixz left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Commenting first for the Python and the documentation part (some comments I left last year should still be addressed but those are mainly for rendering docs or tests).

Comment thread Doc/library/uuid.rst
variant and version number set according to :rfc:`9562`, overriding bits in the
given *hex*, *bytes*, *bytes_le*, *fields*, or *int*.

.. versionchanged:: 3.16

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
.. versionchanged:: 3.16
.. versionchanged:: next

Comment thread Lib/test/test_uuid.py
Comment on lines +392 to +393
self.assertEqual(
self.uuid.UUID(*kwargs.values(), version=None), expected)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
self.assertEqual(
self.uuid.UUID(*kwargs.values(), version=None), expected)
u_with_args = self.uuid.UUID(*kwargs.values(), version=None)
self.assertEqual(u_with_args, expected)

Comment thread Lib/test/test_uuid.py
with self.subTest(input=name):
kwargs = dict.fromkeys(inputs)
kwargs[name] = value
self.assertEqual(self.uuid.UUID(**kwargs, version=None), expected)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
self.assertEqual(self.uuid.UUID(**kwargs, version=None), expected)
u_with_kwargs = self.uuid.UUID(**kwargs, version=None)
self.assertEqual(u_with_kwargs, expected)

Comment thread Lib/test/test_uuid.py
for safety in self.uuid.SafeUUID
)
with support.swap_item(sys.modules, 'uuid', self.uuid):
for mapping in (dict, UserDict, MappingProxyType):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
for mapping in (dict, UserDict, MappingProxyType):
for mapping in (dict, frozendict, UserDict, MappingProxyType):

Comment thread Lib/test/test_uuid.py

def test_setstate_missing_int(self):
# A missing required key must retain the mapping's KeyError and key.
for mapping in (dict, UserDict, MappingProxyType):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
for mapping in (dict, UserDict, MappingProxyType):
for mapping in (dict, frozendict, UserDict, MappingProxyType):

Comment thread Lib/uuid.py
Comment on lines +978 to 981
_c_uuid4 = uuid4
_c_uuid7 = uuid7

def main():

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
_c_uuid4 = uuid4
_c_uuid7 = uuid7
def main():
_c_uuid4 = uuid4
_c_uuid7 = uuid7
def main():

Comment thread Lib/uuid.py
_py_UUID = UUID
try:
from _uuid import UUID, uuid4, uuid7
from _uuid import _install_c_hooks as _install_c_hooks

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure about the as _install_c_hooks relevance.

Comment thread Lib/uuid.py
Comment on lines +973 to +975
_c_UUID = None
_c_uuid4 = None
_c_uuid7 = None

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
_c_UUID = None
_c_uuid4 = None
_c_uuid7 = None
_c_UUID = _c_uuid4 = _c_uuid7 = None

Comment thread Lib/uuid.py
Comment on lines +966 to +968
_py_uuid4 = uuid4
_py_uuid7 = uuid7
_py_UUID = UUID

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
_py_uuid4 = uuid4
_py_uuid7 = uuid7
_py_UUID = UUID
_py_UUID = UUID
_py_uuid4 = uuid4
_py_uuid7 = uuid7

Comment on lines +1 to +2
Reimplement base UUID type, uuid4(), and uuid7() in C

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Reimplement base UUID type, uuid4(), and uuid7() in C
:mod:`uuid`: Reimplement :class:`~uuid.UUID` objects, :func:`~uuid.uuid4`,
and :func:`~uuid.uuid7` in C.

Just to get some nice markup in the changelog.

@picnixz picnixz left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mostly LGTM with a few nitpicks and questions (about __str__/__repr__/__setattr__).

Comment thread Modules/_uuidmodule.c

*counter = (((uint64_t)(high & 0x1FF) << 32) | (low >> 32)) & 0x1FFFFFFFFFF;
*tail = (uint32_t)low;
return 0;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@1st1 for visibility

Comment thread Modules/_uuidmodule.c

/*[clinic input]
@critical_section
_uuid._install_c_hooks

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add a docstring for this one to say "Only for testing purposes."

Comment thread Modules/_uuidmodule.c
int i, j;

// Reimplement `hex = hex.replace('urn:', '').replace('uuid:', '')`
if (size > 0 && start[0] == 'u') {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a slight difference here where uuid:urn:... is also supported in the pure Python version. So what we could do is reject it in the pure Python version as well as it's not a legitimate change. More precisely, what is allowed are prefixes urn:uuid:, urn: and uuid:.

I would suggest that we change the pure Python implementation so that we only parse those forms and not those that contain uuid in the middle of the string like abcuuid:def (which would be 'abcdef').

Looking at one of your other comment, if you want to retain exact same semantics, you need to really re-implement the substring replacement (not just the prefix replacement).

Comment thread Modules/_uuidmodule.c
Comment on lines +692 to +698
if (size >= 1 && start[0] == '{') {
start++;
size -= 1;
}
if (size >= 1 && start[size - 1] == '}') {
size -= 1;
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Strictly speaking you need a while loop here for the strip. Either do it with a while loop or change the Python implementation to removeprefix('{') and removesuffix('}') so that we only strip one character and not consecutive ones.

Comment thread Modules/_uuidmodule.c
Comment on lines +763 to +765
if (PyBytes_GET_SIZE(bytes_le) != UUID_SIZE) {
PyErr_SetString(PyExc_ValueError,
"bytes_le is not a 16-char string");

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (PyBytes_GET_SIZE(bytes_le) != UUID_SIZE) {
PyErr_SetString(PyExc_ValueError,
"bytes_le is not a 16-char string");
if (PyBytes_GET_SIZE(bytes_le) != UUID_SIZE) {
PyErr_Format(PyExc_ValueError,
"bytes_le is not a %d-char string", UUID_SIZE);

Comment thread Modules/_uuidmodule.c
// UUID string format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx (36 chars)
char str[36];

for (int i = 0; i < 4; i++) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you unrolled the loops, is it faster?

Comment thread Modules/_uuidmodule.c
}

// Get the class name (sadly can't use tp_name -- we don't need the full name)
PyObject *cls_name = PyObject_GetAttrString((PyObject *)Py_TYPE(self), "__name__");

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@1st1 for visibility

Comment thread Modules/_uuidmodule.c
return NULL;
}

PyObject *repr = PyUnicode_FromFormat("%U('%U')", cls_name, str_obj);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't remember now but if someone subclass the UUID class and has a different __str__, wouldn't it be better if we use %U('%S') then? using PyType_GetName() should also be enought.

Comment thread Modules/_uuidmodule.c
}

static int
Uuid_setattr(PyObject *Py_UNUSED(self), PyObject *Py_UNUSED(name),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we really need to override the __setattr__? aren't we by default read-only here?

Comment thread Modules/_uuidmodule.c
Comment on lines +1832 to +1837
state->uuid_type = (PyTypeObject *)PyType_FromMetaclass(
NULL,
module,
&Uuid_spec,
NULL
);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
state->uuid_type = (PyTypeObject *)PyType_FromMetaclass(
NULL,
module,
&Uuid_spec,
NULL
);
state->uuid_type = (PyTypeObject *)PyType_FromModuleAndSpec(
module,
&Uuid_spec,
NULL
);

I think it's enough that way.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

awaiting core review type-feature A feature request or enhancement

Projects

None yet

Development

Successfully merging this pull request may close these issues.

9 participants