From: Daniele Varrazzo Date: Sat, 8 Feb 2025 15:36:25 +0000 (+0100) Subject: perf(uuid): speed up UUID creation using a writable subclass X-Git-Tag: 3.2.5~2^2~2 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=393e162fc377f6064ca78a3d11d4a8e9e9b84272;p=thirdparty%2Fpsycopg.git perf(uuid): speed up UUID creation using a writable subclass Introduce an object memory-compatible with UUID, but writable. Try to create this object in the fastest possible way: calling __new__ and setting its attributes. Then replace the class with the standard UUID. --- diff --git a/psycopg_c/psycopg_c/_uuid.py b/psycopg_c/psycopg_c/_uuid.py new file mode 100644 index 000000000..ec6b5338f --- /dev/null +++ b/psycopg_c/psycopg_c/_uuid.py @@ -0,0 +1,28 @@ +""" +Internal objects to support the UUID adapters. +""" + +# Copyright (C) 2025 The Psycopg Team + +import uuid + +# Re-exports +UUID = uuid.UUID +SafeUUID_unknown = uuid.SafeUUID.unknown + + +class _WritableUUID(UUID): + """Temporary class, with the same memory layout of UUID, but writable. + + This class must have the same memory layout of the UUID class, so we can + create one, setting the `int` attribute, and changing the `__class__`, + which should be faster than calling the complex UUID.__init__ machinery. + + u = _WritableUUID() + u.is_safe = ... + u.int = ... + u.__class__ = UUID + """ + + __slots__ = () # Give the class the same memory layout of the base clasee + __setattr__ = object.__setattr__ # make the class writable diff --git a/psycopg_c/psycopg_c/types/uuid.pyx b/psycopg_c/psycopg_c/types/uuid.pyx index e9be3b680..81f03e5ef 100644 --- a/psycopg_c/psycopg_c/types/uuid.pyx +++ b/psycopg_c/psycopg_c/types/uuid.pyx @@ -10,9 +10,6 @@ cdef extern from "Python.h": const char *PyUnicode_AsUTF8(object unicode) except NULL -uuid: ModuleType | None = None - - @cython.final cdef class UUIDDumper(CDumper): format = PQ_TEXT @@ -61,25 +58,37 @@ static const int8_t hex_to_int_map[] = { const int8_t[256] hex_to_int_map -@cython.final -cdef class UUIDLoader(CLoader): - format = PQ_TEXT +cdef class _UUIDLoader(CLoader): - cdef PyObject *_uuid_type - cdef object _uuid_new - cdef object _obj_setattr - cdef PyObject *_safeuuid_unknown + cdef object _object_new + cdef object _uuid_type + cdef PyObject *_wuuid_type + cdef object _safeuuid_unknown def __cinit__(self, oid: int, context: AdaptContext | None = None): - global uuid - # uuid is slow to import, lazy load it - if uuid is None: - import uuid + from psycopg_c import _uuid + + self._object_new = object.__new__ + self._uuid_type = _uuid.UUID + self._wuuid_type = _uuid._WritableUUID + self._safeuuid_unknown = _uuid.SafeUUID_unknown + + cdef object _return_uuid(self, uint64_t low, uint64_t high): + cdef object py_low = PyLong_FromUnsignedLongLong(low) + cdef object py_high = PyLong_FromUnsignedLongLong(high) + cdef object py_value = (py_high << 64) | py_low + + cdef object u = PyObject_CallFunctionObjArgs( + self._object_new, self._wuuid_type, NULL) + u.int = py_value + u.is_safe = self._safeuuid_unknown + u.__class__ = self._uuid_type + return u + - self._uuid_type = uuid.UUID - self._uuid_new = uuid.UUID.__new__ - self._obj_setattr = object.__setattr__ - self._safeuuid_unknown = uuid.SafeUUID.unknown +@cython.final +cdef class UUIDLoader(_UUIDLoader): + format = PQ_TEXT cdef object cload(self, const char *data, size_t length): cdef uint64_t high = 0 @@ -102,37 +111,19 @@ cdef class UUIDLoader(CLoader): if ndigits != 32: raise ValueError("Invalid UUID string") - cdef object int_value = (PyLong_FromUnsignedLongLong(high) << 64) | PyLong_FromUnsignedLongLong(low) - - cdef object u = PyObject_CallFunctionObjArgs(self._uuid_new, self._uuid_type, NULL) - PyObject_CallFunctionObjArgs(self._obj_setattr, u, "is_safe", self._safeuuid_unknown, NULL) - PyObject_CallFunctionObjArgs(self._obj_setattr, u, "int", int_value, NULL) - return u + return self._return_uuid(low, high) @cython.final -cdef class UUIDBinaryLoader(CLoader): +cdef class UUIDBinaryLoader(_UUIDLoader): format = PQ_BINARY - cdef PyObject *_uuid_type - cdef object _uuid_new - cdef object _obj_setattr - cdef PyObject *_safeuuid_unknown - - def __cinit__(self, oid: int, context: AdaptContext | None = None): - global uuid - # uuid is slow to import, lazy load it - if uuid is None: - import uuid - - self._uuid_type = uuid.UUID - self._uuid_new = uuid.UUID.__new__ - self._obj_setattr = object.__setattr__ - self._safeuuid_unknown = uuid.SafeUUID.unknown - cdef object cload(self, const char *data, size_t length): - cdef object int_value = int.from_bytes(data[:length], 'big') - cdef object u = PyObject_CallFunctionObjArgs(self._uuid_new, self._uuid_type, NULL) - PyObject_CallFunctionObjArgs(self._obj_setattr, u, "is_safe", self._safeuuid_unknown, NULL) - PyObject_CallFunctionObjArgs(self._obj_setattr, u, "int", int_value, NULL) - return u + cdef uint64_t be[2] + if length != sizeof(be): + raise ValueError("Invalid UUID data") + memcpy(&be, data, sizeof(be)) + + cdef uint64_t high = endian.be64toh(be[0]) + cdef uint64_t low = endian.be64toh(be[1]) + return self._return_uuid(low, high)