From 74f473684a76a13753d2aac0fc82714f169b610a Mon Sep 17 00:00:00 2001 From: Sergey B Kirpichev Date: Wed, 8 Jul 2026 08:15:37 +0300 Subject: [PATCH 1/3] Rename cache -> freelist --- gmp.c | 50 ++++++++++++++++++++++++++++------------------- tests/test_mpz.py | 5 ++++- 2 files changed, 34 insertions(+), 21 deletions(-) diff --git a/gmp.c b/gmp.c index 0e4737e0..fb504c85 100644 --- a/gmp.c +++ b/gmp.c @@ -7,21 +7,19 @@ #include #include -#if !defined(PYPY_VERSION) -# define MAX_CACHE_SIZE 100 -#else -# define MAX_CACHE_SIZE 0 -#endif -#define MAX_CACHED_SIZEOF 256 +#ifndef PYPY_VERSION +# define MAX_FREELIST_SIZE 100 +# define MAX_FREELIST_SIZEOF 256 typedef struct { - MPZ_Object *gmp_cache[MAX_CACHE_SIZE + 1]; - size_t gmp_cache_size; + MPZ_Object *freelist[MAX_FREELIST_SIZE + 1]; + size_t freelist_size; } gmp_global; _Thread_local gmp_global global = { - .gmp_cache_size = 0, + .freelist_size = 0, }; +#endif uint8_t bits_per_digit; Py_hash_t pyhash_modulus; @@ -31,12 +29,14 @@ MPZ_new(void) { MPZ_Object *res; - if (global.gmp_cache_size) { - res = global.gmp_cache[--global.gmp_cache_size]; +#ifndef PYPY_VERSION + if (global.freelist_size) { + res = global.freelist[--global.freelist_size]; (void)zz_set(0, &res->z); - Py_XINCREF((PyObject *)res); + Py_INCREF((PyObject *)res); } else { +#endif res = PyObject_New(MPZ_Object, &MPZ_Type); if (!res) { return NULL; /* LCOV_EXCL_LINE */ @@ -44,7 +44,9 @@ MPZ_new(void) if (zz_init(&res->z)) { return (MPZ_Object *)PyErr_NoMemory(); /* LCOV_EXCL_LINE */ } +#ifndef PYPY_VERSION } +#endif res->hash_cache = -1; return res; } @@ -644,13 +646,15 @@ dealloc(PyObject *self) { MPZ_Object *u = (MPZ_Object *)self; - if (global.gmp_cache_size < MAX_CACHE_SIZE - && zz_sizeof(&u->z) <= MAX_CACHED_SIZEOF +#ifndef PYPY_VERSION + if (global.freelist_size < MAX_FREELIST_SIZE + && zz_sizeof(&u->z) <= MAX_FREELIST_SIZEOF && MPZ_CheckExact(self)) { - global.gmp_cache[global.gmp_cache_size++] = u; + global.freelist[global.freelist_size++] = u; } else { +#endif freefunc tp_free; if (MPZ_CheckExact(self)) { @@ -661,7 +665,9 @@ dealloc(PyObject *self) } zz_clear(&u->z); tp_free(self); +#ifndef PYPY_VERSION } +#endif } static PyObject * @@ -2611,11 +2617,12 @@ gmp__mpmath_create(PyObject *self, PyObject *const *args, Py_ssize_t nargs) return Py_BuildValue("(bNNK)", negative, man, iexp, bc); } +#ifndef PYPY_VERSION static PyObject * -gmp__free_cache(PyObject *Py_UNUSED(module), PyObject *Py_UNUSED(args)) +gmp__clear_freelist(PyObject *Py_UNUSED(module), PyObject *Py_UNUSED(args)) { - while (global.gmp_cache_size) { - MPZ_Object *u = global.gmp_cache[--global.gmp_cache_size]; + while (global.freelist_size) { + MPZ_Object *u = global.freelist[--global.freelist_size]; PyObject *self = (PyObject *)u; zz_clear(&u->z); @@ -2623,6 +2630,7 @@ gmp__free_cache(PyObject *Py_UNUSED(module), PyObject *Py_UNUSED(args)) } Py_RETURN_NONE; } +#endif static PyMethodDef gmp_functions[] = { {"gcd", (PyCFunction)gmp_gcd, METH_FASTCALL, @@ -2656,8 +2664,10 @@ static PyMethodDef gmp_functions[] = { {"_mpmath_create", (PyCFunction)gmp__mpmath_create, METH_FASTCALL, ("_mpmath_create($module, man, exp, prec=0, rnd='d', /)\n--\n\n" "Helper function for mpmath.")}, - {"_free_cache", gmp__free_cache, METH_NOARGS, - "_free_cache($module)\n--\n\nFree mpz's cache."}, +#ifndef PYPY_VERSION + {"_clear_freelist", gmp__clear_freelist, METH_NOARGS, + "_clear_freelist($module)\n--\n\nFree mpz's cache."}, +#endif {NULL} /* sentinel */ }; diff --git a/tests/test_mpz.py b/tests/test_mpz.py index ab89ca49..7ab14dff 100644 --- a/tests/test_mpz.py +++ b/tests/test_mpz.py @@ -10,6 +10,7 @@ from concurrent.futures import ThreadPoolExecutor from subprocess import run +import gmp import pytest from gmp import mpz from hypothesis import assume, example, given, settings @@ -1095,8 +1096,10 @@ def test_int_api(): assert m_sig == mz_sig +@pytest.mark.skipif(not hasattr(gmp, "_clear_freelist"), + reason="no support for mpz cache") def test_mpz_clear(): # for coverage (test module cleanup) res = run([sys.executable, "-c", - "import gmp; a = gmp.mpz(1); del a; gmp._free_cache()"]) + "import gmp; a = gmp.mpz(1); del a; gmp._clear_freelist()"]) assert res.returncode == 0 From aa1140c1079b3ea9036cf50b9043b74366205682 Mon Sep 17 00:00:00 2001 From: Sergey B Kirpichev Date: Wed, 8 Jul 2026 08:17:02 +0300 Subject: [PATCH 2/3] Drop _clear_freelist() --- gmp.c | 19 ------------------- tests/test_mpz.py | 11 ----------- 2 files changed, 30 deletions(-) diff --git a/gmp.c b/gmp.c index fb504c85..139d1bb0 100644 --- a/gmp.c +++ b/gmp.c @@ -2617,21 +2617,6 @@ gmp__mpmath_create(PyObject *self, PyObject *const *args, Py_ssize_t nargs) return Py_BuildValue("(bNNK)", negative, man, iexp, bc); } -#ifndef PYPY_VERSION -static PyObject * -gmp__clear_freelist(PyObject *Py_UNUSED(module), PyObject *Py_UNUSED(args)) -{ - while (global.freelist_size) { - MPZ_Object *u = global.freelist[--global.freelist_size]; - PyObject *self = (PyObject *)u; - - zz_clear(&u->z); - PyObject_Free(self); - } - Py_RETURN_NONE; -} -#endif - static PyMethodDef gmp_functions[] = { {"gcd", (PyCFunction)gmp_gcd, METH_FASTCALL, ("gcd($module, /, *integers)\n--\n\n" @@ -2664,10 +2649,6 @@ static PyMethodDef gmp_functions[] = { {"_mpmath_create", (PyCFunction)gmp__mpmath_create, METH_FASTCALL, ("_mpmath_create($module, man, exp, prec=0, rnd='d', /)\n--\n\n" "Helper function for mpmath.")}, -#ifndef PYPY_VERSION - {"_clear_freelist", gmp__clear_freelist, METH_NOARGS, - "_clear_freelist($module)\n--\n\nFree mpz's cache."}, -#endif {NULL} /* sentinel */ }; diff --git a/tests/test_mpz.py b/tests/test_mpz.py index 7ab14dff..146987a6 100644 --- a/tests/test_mpz.py +++ b/tests/test_mpz.py @@ -8,9 +8,7 @@ import sys import warnings from concurrent.futures import ThreadPoolExecutor -from subprocess import run -import gmp import pytest from gmp import mpz from hypothesis import assume, example, given, settings @@ -1094,12 +1092,3 @@ def test_int_api(): continue mz_sig = inspect.signature(mz) assert m_sig == mz_sig - - -@pytest.mark.skipif(not hasattr(gmp, "_clear_freelist"), - reason="no support for mpz cache") -def test_mpz_clear(): - # for coverage (test module cleanup) - res = run([sys.executable, "-c", - "import gmp; a = gmp.mpz(1); del a; gmp._clear_freelist()"]) - assert res.returncode == 0 From 3bcfcb46a7b1e54fef10156f45e1d618194ad6ec Mon Sep 17 00:00:00 2001 From: Sergey B Kirpichev Date: Wed, 8 Jul 2026 08:22:16 +0300 Subject: [PATCH 3/3] Use freelist only on CPython This optimization seems working on the GraalPy, but looks useless: collatz0(871): Mean +- std dev: [nocache-graalpy] 2.19 ms +- 1.14 ms -> [graalpy] 5.12 ms +- 3.54 ms: 2.34x slower collatz0((1<<128)+31): Mean +- std dev: [nocache-graalpy] 25.6 ms +- 13.5 ms -> [graalpy] 17.6 ms +- 14.4 ms: 1.45x faster collatz1(97): Mean +- std dev: [nocache-graalpy] 4.40 ms +- 3.17 ms -> [graalpy] 2.15 ms +- 1.98 ms: 2.05x faster collatz1(871): Mean +- std dev: [nocache-graalpy] 2.49 ms +- 2.22 ms -> [graalpy] 4.84 ms +- 3.92 ms: 1.94x slower collatz2(871): Mean +- std dev: [nocache-graalpy] 2.24 ms +- 1.11 ms -> [graalpy] 5.06 ms +- 3.67 ms: 2.26x slower collatz2((1<<128)+31): Mean +- std dev: [nocache-graalpy] 11.6 ms +- 8.2 ms -> [graalpy] 25.9 ms +- 15.1 ms: 2.23x slower Benchmark hidden because not significant (3): collatz0(97), collatz1((1<<128)+31), collatz2(97) Geometric mean: 1.26x slower While on CPython: collatz0(97): Mean +- std dev: [nocache-cpython] 122 us +- 2 us -> [cpython] 87.4 us +- 1.6 us: 1.40x faster collatz0(871): Mean +- std dev: [nocache-cpython] 184 us +- 3 us -> [cpython] 131 us +- 4 us: 1.40x faster collatz0((1<<128)+31): Mean +- std dev: [nocache-cpython] 968 us +- 17 us -> [cpython] 699 us +- 32 us: 1.38x faster collatz1(97): Mean +- std dev: [nocache-cpython] 152 us +- 4 us -> [cpython] 105 us +- 3 us: 1.45x faster collatz1(871): Mean +- std dev: [nocache-cpython] 231 us +- 13 us -> [cpython] 158 us +- 1 us: 1.47x faster collatz1((1<<128)+31): Mean +- std dev: [nocache-cpython] 1.22 ms +- 0.05 ms -> [cpython] 847 us +- 14 us: 1.43x faster collatz2(97): Mean +- std dev: [nocache-cpython] 124 us +- 2 us -> [cpython] 87.4 us +- 0.4 us: 1.41x faster collatz2(871): Mean +- std dev: [nocache-cpython] 187 us +- 7 us -> [cpython] 131 us +- 1 us: 1.43x faster collatz2((1<<128)+31): Mean +- std dev: [nocache-cpython] 988 us +- 24 us -> [cpython] 703 us +- 2 us: 1.41x faster Geometric mean: 1.42x faster This approach doesn't work on PyPy (triggers "Invalid usage of a dying CPython object" error), but hardly it does make sense here. For instance, following pure-Python benchmark shows same numbers on PyPy, regardless on freelist size: ``` import os import pyperf from operator import add _freelist = [] _max_freelist_size = int(os.getenv("FREELIST_SIZE")) class xyz: def __new__(cls, *args, **kwargs): if _freelist: # Reuse an existing instance from the pool instance = _freelist.pop() return instance # Fall back to standard memory allocation if pool is empty return super().__new__(cls) def __init__(self, value): self.value = value def __del__(self): if len(_freelist) < _max_freelist_size: # Resurrect 'self' by adding it to the free-list _freelist.append(self) def __add__(self, other): return xyz(self.value + other.value) def __repr__(self): return f"xyz('{self.value}')" x, y = map(xyz, [123, 321]) runner = pyperf.Runner() s = repr(x) + " + " + repr(y) runner.bench_func(s, add, x, y) ``` Closes #301 --- gmp.c | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/gmp.c b/gmp.c index 139d1bb0..1214a26e 100644 --- a/gmp.c +++ b/gmp.c @@ -7,7 +7,12 @@ #include #include -#ifndef PYPY_VERSION +#define ON_CPYTHON 1 +#if defined(PYPY_VERSION) || defined(GRAALVM_PYTHON) +# undef ON_CPYTHON +#endif + +#ifdef ON_CPYTHON # define MAX_FREELIST_SIZE 100 # define MAX_FREELIST_SIZEOF 256 @@ -29,7 +34,7 @@ MPZ_new(void) { MPZ_Object *res; -#ifndef PYPY_VERSION +#ifdef ON_CPYTHON if (global.freelist_size) { res = global.freelist[--global.freelist_size]; (void)zz_set(0, &res->z); @@ -44,7 +49,7 @@ MPZ_new(void) if (zz_init(&res->z)) { return (MPZ_Object *)PyErr_NoMemory(); /* LCOV_EXCL_LINE */ } -#ifndef PYPY_VERSION +#ifdef ON_CPYTHON } #endif res->hash_cache = -1; @@ -646,7 +651,7 @@ dealloc(PyObject *self) { MPZ_Object *u = (MPZ_Object *)self; -#ifndef PYPY_VERSION +#ifdef ON_CPYTHON if (global.freelist_size < MAX_FREELIST_SIZE && zz_sizeof(&u->z) <= MAX_FREELIST_SIZEOF && MPZ_CheckExact(self)) @@ -665,7 +670,7 @@ dealloc(PyObject *self) } zz_clear(&u->z); tp_free(self); -#ifndef PYPY_VERSION +#ifdef ON_CPYTHON } #endif }