Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 17 additions & 27 deletions README.rst
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
Python-GMP
==========

Python extension module, gmp, providing safe bindings to the GNU GMP (version
6.3.0 or later required) via the `ZZ library <https://github.com/diofant/zz>`_.
This module shouldn't crash the interpreter.
Python extension module, providing bindings to the GNU GMP via the `ZZ library
<https://github.com/diofant/zz>`_. This module shouldn't crash the interpreter.

The gmp can be used as a `gmpy2`_/`python-flint`_ replacement to provide
integer type (`mpz`_), compatible with Python's `int`_. It includes few
functions (`comb`_, `factorial`_, `gcd`_, `isqrt`_, `lcm`_ and `perm`_),
compatible with the Python stdlib's module `math`_.
integer type (`mpz`_), compatible with Python's `int`_. It also includes
functions, compatible with the Python stdlib's submodule `math.integer
<https://docs.python.org/3.15/library/math.integer.html>`_.

This module requires Python 3.11 or later versions and has been tested with
CPython 3.11 through 3.14, with PyPy3.11 7.3.20 and with GraalPy 25.0.
Expand All @@ -22,17 +21,16 @@ Motivation
----------

The CPython (and most other Python implementations, like PyPy) is optimized to
work with small integers. Algorithms used here for "big enough" integers
usually aren't best known in the field. Fortunately, it's possible to use
bindings (for example, the `gmpy2`_ package) to the GNU Multiple Precision
Arithmetic Library (GMP), which aims to be faster than any other bignum library
for all operand sizes.

But such extension modules usually rely on default GMP's memory allocation
functions and can't recover from errors such as out of memory. So, it's easy
to crash the Python interpreter during the interactive session. Following
example with the gmpy2 will work if you set address space limit for the Python
interpreter (e.g. by ``prlimit`` command on Linux):
work with small (machine-sized) integers. Algorithms used here for big
integers usually aren't best known in the field. Fortunately, it's possible to
use bindings (for example, the `gmpy2`_ package) to the GNU GMP, which aims to
be faster than any other bignum library for all operand sizes.

But such extension modules usually rely on default GMP's memory management and
can't recover from allocation failure. So, it's easy to crash the Python
interpreter during the interactive session. Following example with the gmpy2
will work if you set address space limit for the Python interpreter (e.g. by
``prlimit`` command on Linux):

.. code:: pycon

Expand Down Expand Up @@ -70,19 +68,11 @@ Warning on --disable-alloca configure option
--------------------------------------------

You should use the GNU GMP library, compiled with the '--disable-alloca'
configure option to prevent using alloca() for temporary workspace allocation
(and use the heap instead), or this module can't prevent a crash in case of a
stack overflow.
configure option to prevent using alloca() for temporary workspace allocation,
or this module may crash the interpreter in case of a stack overflow.


.. _gmpy2: https://pypi.org/project/gmpy2/
.. _python-flint: https://pypi.org/project/python-flint/
.. _mpz: https://python-gmp.readthedocs.io/en/latest/#gmp.mpz
.. _int: https://docs.python.org/3/library/functions.html#int
.. _factorial: https://python-gmp.readthedocs.io/en/latest/#gmp.factorial
.. _gcd: https://python-gmp.readthedocs.io/en/latest/#gmp.gcd
.. _isqrt: https://python-gmp.readthedocs.io/en/latest/#gmp.isqrt
.. _lcm: https://python-gmp.readthedocs.io/en/latest/#gmp.lcm
.. _comb: https://python-gmp.readthedocs.io/en/latest/#gmp.comb
.. _perm: https://python-gmp.readthedocs.io/en/latest/#gmp.perm
.. _math: https://docs.python.org/3/library/math.html#number-theoretic-functions
11 changes: 11 additions & 0 deletions bench/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
This directory holds some basic benchmarks for the gmp extension.

It's possible to run them also with gmpy2's and flint's integer types:

.. code:: sh

( export T="gmpy2.mpz"; \
python bench/mul.py -q --copy-env --rigorous -o $T.json )

Beware, that the gmp prefers clang over gcc and extensions might
use different compiler options per default.
42 changes: 42 additions & 0 deletions bench/collatz.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# collatz.py

import os

import pyperf

if os.getenv("T") == "gmpy2.mpz":
from gmpy2 import mpz
elif os.getenv("T") == "flint.fmpz":
from flint import fmpz as mpz
else:
from gmp import mpz

zero = mpz(0)
one = mpz(1)
two = mpz(2)
three = mpz(3)

# https://en.wikipedia.org/wiki/Collatz_conjecture

def collatz0(n):
total = 0
n = mpz(n)
while n > one:
n = n*three + one if n & one else n//two
total += 1
return total

def collatz1(n):
total = 0
n = mpz(n)
while n > 1:
n = n*3 + 1 if n & 1 else n//2
total += 1
return total

runner = pyperf.Runner()
for f in [collatz0, collatz1]:
for v in ["97", "871", "(1<<128)+31"]:
h = f"{f.__name__}({v})"
i = eval(v)
runner.bench_func(h, f, i)
24 changes: 24 additions & 0 deletions bench/mul.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# mul.py

import os
from operator import mul

import pyperf

if os.getenv("T") == "gmpy2.mpz":
from gmpy2 import mpz
elif os.getenv("T") == "flint.fmpz":
from flint import fmpz as mpz
else:
from gmp import mpz

values = ["1<<7", "1<<38", "1<<300", "1<<3000"]

runner = pyperf.Runner()
for v in values:
i = eval(v)
bn = '("'+v+'")**2'
x = mpz(i)
# make y != x to avoid a quick mpn_sqr() path on gmp/flint
y = mpz(i + 1)
runner.bench_func(bn, mul, x, y)
10 changes: 7 additions & 3 deletions gmp.c
Original file line number Diff line number Diff line change
Expand Up @@ -2739,9 +2739,13 @@ static PyMethodDef gmp_functions[] = {
("perm($module, n, k=None, /)\n--\n\nNumber of ways to choose k"
" items from n items without repetition and with order.")},
{"_mpmath_normalize", (PyCFunction)gmp__mpmath_normalize, METH_FASTCALL,
NULL},
{"_mpmath_create", (PyCFunction)gmp__mpmath_create, METH_FASTCALL, NULL},
{"_free_cache", gmp__free_cache, METH_NOARGS, "Free mpz's cache."},
("_mpmath_normalize($module, sign, man, exp, bc, prec, rnd, /)\n--\n\n"
"Helper function for mpmath.")},
{"_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."},
{NULL} /* sentinel */
};

Expand Down
2 changes: 1 addition & 1 deletion meson.build
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
project('gmp', 'c',
version: run_command('python', '-m', 'setuptools_scm',
version: run_command('python', 'scripts/gitversion.py',
check: true).stdout().strip(),
default_options: ['c_std=c17'])
py = import('python').find_installation(pure: false)
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[build-system]
build-backend = "mesonpy"
requires = ["meson-python", "setuptools_scm[toml]>=9"]
requires = ["meson-python"]

[project]
name = "python-gmp"
Expand Down
39 changes: 39 additions & 0 deletions scripts/gitversion.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
#!/usr/bin/env python3
import os
import re
import subprocess


def git_version():
# Append last commit date and hash to dev version information,
# if available
return version, git_hash


if __name__ == "__main__":
try:
p = subprocess.Popen(["git", "describe"],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
cwd=os.path.dirname(__file__))
except FileNotFoundError:
exit(1)
else:
out, err = p.communicate()
if p.returncode:
exit(p.returncode)
out = out.decode("ascii").removesuffix("\n")

version, *other = out.removesuffix("\n").split("-")
if other:
g, h = other
m = re.match("(.*)([0-9]+)", version)
version = m[1] + str(int(m[2])+1) + ".dev" + g
git_hash = h
else:
git_hash = ""

if git_hash:
version += "+" + git_hash
print(version)
exit(0)