Skip to content

Commit 9392379

Browse files
markshannonencukou
andauthored
GH-101291: Add low level, unstable API for pylong (GH-101685)
Co-authored-by: Petr Viktorin <[email protected]>
1 parent ab71acd commit 9392379

File tree

8 files changed

+139
-20
lines changed

8 files changed

+139
-20
lines changed

Doc/c-api/long.rst

+24
Original file line numberDiff line numberDiff line change
@@ -322,3 +322,27 @@ distinguished from a number. Use :c:func:`PyErr_Occurred` to disambiguate.
322322
with :c:func:`PyLong_FromVoidPtr`.
323323
324324
Returns ``NULL`` on error. Use :c:func:`PyErr_Occurred` to disambiguate.
325+
326+
327+
.. c:function:: int PyUnstable_Long_IsCompact(const PyLongObject* op)
328+
329+
Return 1 if *op* is compact, 0 otherwise.
330+
331+
This function makes it possible for performance-critical code to implement
332+
a “fast path” for small integers. For compact values use
333+
:c:func:`PyUnstable_Long_CompactValue`; for others fall back to a
334+
:c:func:`PyLong_As* <PyLong_AsSize_t>` function or
335+
:c:func:`calling <PyObject_CallMethod>` :meth:`int.to_bytes`.
336+
337+
The speedup is expected to be negligible for most users.
338+
339+
Exactly what values are considered compact is an implementation detail
340+
and is subject to change.
341+
342+
.. c:function:: Py_ssize_t PyUnstable_Long_CompactValue(const PyLongObject* op)
343+
344+
If *op* is compact, as determined by :c:func:`PyUnstable_Long_IsCompact`,
345+
return its value.
346+
347+
Otherwise, the return value is undefined.
348+

Include/cpython/longintrepr.h

+26
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,32 @@ PyAPI_FUNC(PyLongObject *)
9898
_PyLong_FromDigits(int negative, Py_ssize_t digit_count, digit *digits);
9999

100100

101+
/* Inline some internals for speed. These should be in pycore_long.h
102+
* if user code didn't need them inlined. */
103+
104+
#define _PyLong_SIGN_MASK 3
105+
#define _PyLong_NON_SIZE_BITS 3
106+
107+
static inline int
108+
_PyLong_IsCompact(const PyLongObject* op) {
109+
assert(PyLong_Check(op));
110+
return op->long_value.lv_tag < (2 << _PyLong_NON_SIZE_BITS);
111+
}
112+
113+
#define PyUnstable_Long_IsCompact _PyLong_IsCompact
114+
115+
static inline Py_ssize_t
116+
_PyLong_CompactValue(const PyLongObject *op)
117+
{
118+
assert(PyLong_Check(op));
119+
assert(PyUnstable_Long_IsCompact(op));
120+
Py_ssize_t sign = 1 - (op->long_value.lv_tag & _PyLong_SIGN_MASK);
121+
return sign * (Py_ssize_t)op->long_value.ob_digit[0];
122+
}
123+
124+
#define PyUnstable_Long_CompactValue _PyLong_CompactValue
125+
126+
101127
#ifdef __cplusplus
102128
}
103129
#endif

Include/cpython/longobject.h

+5
Original file line numberDiff line numberDiff line change
@@ -93,3 +93,8 @@ PyAPI_FUNC(PyObject *) _PyLong_GCD(PyObject *, PyObject *);
9393

9494
PyAPI_FUNC(PyObject *) _PyLong_Rshift(PyObject *, size_t);
9595
PyAPI_FUNC(PyObject *) _PyLong_Lshift(PyObject *, size_t);
96+
97+
98+
PyAPI_FUNC(int) PyUnstable_Long_IsCompact(const PyLongObject* op);
99+
PyAPI_FUNC(Py_ssize_t) PyUnstable_Long_CompactValue(const PyLongObject* op);
100+

Include/internal/pycore_long.h

+15-20
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,21 @@ PyAPI_FUNC(char*) _PyLong_FormatBytesWriter(
118118
#define SIGN_NEGATIVE 2
119119
#define NON_SIZE_BITS 3
120120

121+
/* The functions _PyLong_IsCompact and _PyLong_CompactValue are defined
122+
* in Include/cpython/longobject.h, since they need to be inline.
123+
*
124+
* "Compact" values have at least one bit to spare,
125+
* so that addition and subtraction can be performed on the values
126+
* without risk of overflow.
127+
*
128+
* The inline functions need tag bits.
129+
* For readability, rather than do `#define SIGN_MASK _PyLong_SIGN_MASK`
130+
* we define them to the numbers in both places and then assert that
131+
* they're the same.
132+
*/
133+
static_assert(SIGN_MASK == _PyLong_SIGN_MASK, "SIGN_MASK does not match _PyLong_SIGN_MASK");
134+
static_assert(NON_SIZE_BITS == _PyLong_NON_SIZE_BITS, "NON_SIZE_BITS does not match _PyLong_NON_SIZE_BITS");
135+
121136
/* All *compact" values are guaranteed to fit into
122137
* a Py_ssize_t with at least one bit to spare.
123138
* In other words, for 64 bit machines, compact
@@ -131,11 +146,6 @@ _PyLong_IsNonNegativeCompact(const PyLongObject* op) {
131146
return op->long_value.lv_tag <= (1 << NON_SIZE_BITS);
132147
}
133148

134-
static inline int
135-
_PyLong_IsCompact(const PyLongObject* op) {
136-
assert(PyLong_Check(op));
137-
return op->long_value.lv_tag < (2 << NON_SIZE_BITS);
138-
}
139149

140150
static inline int
141151
_PyLong_BothAreCompact(const PyLongObject* a, const PyLongObject* b) {
@@ -144,21 +154,6 @@ _PyLong_BothAreCompact(const PyLongObject* a, const PyLongObject* b) {
144154
return (a->long_value.lv_tag | b->long_value.lv_tag) < (2 << NON_SIZE_BITS);
145155
}
146156

147-
/* Returns a *compact* value, iff `_PyLong_IsCompact` is true for `op`.
148-
*
149-
* "Compact" values have at least one bit to spare,
150-
* so that addition and subtraction can be performed on the values
151-
* without risk of overflow.
152-
*/
153-
static inline Py_ssize_t
154-
_PyLong_CompactValue(const PyLongObject *op)
155-
{
156-
assert(PyLong_Check(op));
157-
assert(_PyLong_IsCompact(op));
158-
Py_ssize_t sign = 1 - (op->long_value.lv_tag & SIGN_MASK);
159-
return sign * (Py_ssize_t)op->long_value.ob_digit[0];
160-
}
161-
162157
static inline bool
163158
_PyLong_IsZero(const PyLongObject *op)
164159
{

Lib/test/test_capi/test_long.py

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import unittest
2+
import sys
3+
4+
from test.support import import_helper
5+
6+
# Skip this test if the _testcapi module isn't available.
7+
_testcapi = import_helper.import_module('_testcapi')
8+
9+
10+
class LongTests(unittest.TestCase):
11+
12+
def test_compact(self):
13+
for n in {
14+
# Edge cases
15+
*(2**n for n in range(66)),
16+
*(-2**n for n in range(66)),
17+
*(2**n - 1 for n in range(66)),
18+
*(-2**n + 1 for n in range(66)),
19+
# Essentially random
20+
*(37**n for n in range(14)),
21+
*(-37**n for n in range(14)),
22+
}:
23+
with self.subTest(n=n):
24+
is_compact, value = _testcapi.call_long_compact_api(n)
25+
if is_compact:
26+
self.assertEqual(n, value)
27+
28+
def test_compact_known(self):
29+
# Sanity-check some implementation details (we don't guarantee
30+
# that these are/aren't compact)
31+
self.assertEqual(_testcapi.call_long_compact_api(-1), (True, -1))
32+
self.assertEqual(_testcapi.call_long_compact_api(0), (True, 0))
33+
self.assertEqual(_testcapi.call_long_compact_api(256), (True, 256))
34+
self.assertEqual(_testcapi.call_long_compact_api(sys.maxsize),
35+
(False, -1))
36+
37+
38+
if __name__ == "__main__":
39+
unittest.main()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Added unstable C API for extracting the value of "compact" integers:
2+
:c:func:`PyUnstable_Long_IsCompact` and
3+
:c:func:`PyUnstable_Long_CompactValue`.

Modules/_testcapi/long.c

+13
Original file line numberDiff line numberDiff line change
@@ -534,6 +534,18 @@ test_long_numbits(PyObject *self, PyObject *Py_UNUSED(ignored))
534534
Py_RETURN_NONE;
535535
}
536536

537+
static PyObject *
538+
check_long_compact_api(PyObject *self, PyObject *arg)
539+
{
540+
assert(PyLong_Check(arg));
541+
int is_compact = PyUnstable_Long_IsCompact((PyLongObject*)arg);
542+
Py_ssize_t value = -1;
543+
if (is_compact) {
544+
value = PyUnstable_Long_CompactValue((PyLongObject*)arg);
545+
}
546+
return Py_BuildValue("in", is_compact, value);
547+
}
548+
537549
static PyMethodDef test_methods[] = {
538550
{"test_long_and_overflow", test_long_and_overflow, METH_NOARGS},
539551
{"test_long_api", test_long_api, METH_NOARGS},
@@ -543,6 +555,7 @@ static PyMethodDef test_methods[] = {
543555
{"test_long_long_and_overflow",test_long_long_and_overflow, METH_NOARGS},
544556
{"test_long_numbits", test_long_numbits, METH_NOARGS},
545557
{"test_longlong_api", test_longlong_api, METH_NOARGS},
558+
{"call_long_compact_api", check_long_compact_api, METH_O},
546559
{NULL},
547560
};
548561

Objects/longobject.c

+14
Original file line numberDiff line numberDiff line change
@@ -6366,3 +6366,17 @@ _PyLong_FiniTypes(PyInterpreterState *interp)
63666366
{
63676367
_PyStructSequence_FiniBuiltin(interp, &Int_InfoType);
63686368
}
6369+
6370+
#undef PyUnstable_Long_IsCompact
6371+
6372+
int
6373+
PyUnstable_Long_IsCompact(const PyLongObject* op) {
6374+
return _PyLong_IsCompact(op);
6375+
}
6376+
6377+
#undef PyUnstable_Long_CompactValue
6378+
6379+
Py_ssize_t
6380+
PyUnstable_Long_CompactValue(const PyLongObject* op) {
6381+
return _PyLong_CompactValue(op);
6382+
}

0 commit comments

Comments
 (0)