Skip to content

Commit 62bbec2

Browse files
committed
gh-151289: narrow wide-int fast path to long add/sub
1 parent c22e9c9 commit 62bbec2

3 files changed

Lines changed: 204 additions & 0 deletions

File tree

Lib/test/test_long.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1140,6 +1140,44 @@ def test_small_ints(self):
11401140
self.assertIs(i - i, 0)
11411141
self.assertIs(0 * i, 0)
11421142

1143+
@support.cpython_only
1144+
def test_int64_boundary_add(self):
1145+
INT64_MAX = (1 << 63) - 1
1146+
INT64_MIN = -(1 << 63)
1147+
1148+
self.assertEqual(INT64_MAX + 0, INT64_MAX)
1149+
self.assertEqual(INT64_MAX + (-1), INT64_MAX - 1)
1150+
1151+
self.assertEqual(INT64_MIN + 0, INT64_MIN)
1152+
self.assertEqual(INT64_MIN + 1, INT64_MIN + 1)
1153+
1154+
self.assertEqual(INT64_MAX + 1, 1 << 63)
1155+
self.assertEqual(INT64_MIN + (-1), INT64_MIN - 1)
1156+
1157+
beyond_max = INT64_MAX + 2
1158+
beyond_min = INT64_MIN - 2
1159+
self.assertEqual(beyond_max + 1, INT64_MAX + 3)
1160+
self.assertEqual(beyond_min + (-1), INT64_MIN - 3)
1161+
1162+
@support.cpython_only
1163+
def test_int64_boundary_sub(self):
1164+
INT64_MAX = (1 << 63) - 1
1165+
INT64_MIN = -(1 << 63)
1166+
1167+
self.assertEqual(INT64_MIN - 0, INT64_MIN)
1168+
self.assertEqual(INT64_MIN - (-1), INT64_MIN + 1)
1169+
1170+
self.assertEqual(INT64_MAX - 0, INT64_MAX)
1171+
self.assertEqual(INT64_MAX - 1, INT64_MAX - 1)
1172+
1173+
self.assertEqual(INT64_MIN - 1, INT64_MIN - 1)
1174+
self.assertEqual(INT64_MAX - (-1), 1 << 63)
1175+
1176+
beyond_max = INT64_MAX + 2
1177+
beyond_min = INT64_MIN - 2
1178+
self.assertEqual(beyond_max - 1, INT64_MAX + 1)
1179+
self.assertEqual(beyond_min - (-1), INT64_MIN - 1)
1180+
11431181
def test_bit_length(self):
11441182
tiny = 1e-10
11451183
for x in range(-65000, 65000):
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Improve performance of binary ``int`` add and subtract for wide exact
2+
integers that fit in ``int64_t``.

Objects/longobject.c

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3830,6 +3830,9 @@ x_sub(PyLongObject *a, PyLongObject *b)
38303830
return maybe_small_long(long_normalize(z));
38313831
}
38323832

3833+
static PyLongObject *_PyCompactLong_AddWide(PyLongObject *a, PyLongObject *b);
3834+
static PyLongObject *_PyCompactLong_SubtractWide(PyLongObject *a, PyLongObject *b);
3835+
38333836
static PyLongObject *
38343837
long_add(PyLongObject *a, PyLongObject *b)
38353838
{
@@ -3838,6 +3841,11 @@ long_add(PyLongObject *a, PyLongObject *b)
38383841
return _PyLong_FromSTwoDigits(z);
38393842
}
38403843

3844+
PyLongObject *fast = _PyCompactLong_AddWide(a, b);
3845+
if (fast != NULL || PyErr_Occurred()) {
3846+
return fast;
3847+
}
3848+
38413849
PyLongObject *z;
38423850
if (_PyLong_IsNegative(a)) {
38433851
if (_PyLong_IsNegative(b)) {
@@ -3871,6 +3879,124 @@ _PyCompactLong_Add(PyLongObject *a, PyLongObject *b)
38713879
return medium_from_stwodigits(v);
38723880
}
38733881

3882+
/* Max number of digits a PyLong can have and still fit in int64_t.
3883+
* 30-bit builds: ceil(64/30) = 3. 15-bit builds: ceil(64/15) = 5. */
3884+
#define _PY_LONG_MAX_DIGITS_FOR_INT64 ((64 + PyLong_SHIFT - 1) / PyLong_SHIFT)
3885+
3886+
static inline bool
3887+
_PyLong_TryAsInt64ExactLocal(PyLongObject *v, int64_t *out)
3888+
{
3889+
assert(PyLong_CheckExact((PyObject *)v));
3890+
uintptr_t tag = v->long_value.lv_tag;
3891+
int sign = 1 - (int)(tag & SIGN_MASK);
3892+
if (tag < (2u << NON_SIZE_BITS)) {
3893+
*out = (int64_t)(sign * (Py_ssize_t)v->long_value.ob_digit[0]);
3894+
return true;
3895+
}
3896+
Py_ssize_t ndigits = (Py_ssize_t)(tag >> NON_SIZE_BITS);
3897+
if (ndigits > _PY_LONG_MAX_DIGITS_FOR_INT64) {
3898+
return false;
3899+
}
3900+
uint64_t abs_val = 0;
3901+
#if PyLong_SHIFT == 30
3902+
if (ndigits == 2) {
3903+
abs_val = (uint64_t)v->long_value.ob_digit[0] |
3904+
((uint64_t)v->long_value.ob_digit[1] << 30);
3905+
*out = sign < 0 ? -(int64_t)abs_val : (int64_t)abs_val;
3906+
return true;
3907+
}
3908+
#endif
3909+
unsigned int shift = 0;
3910+
for (Py_ssize_t i = 0; i < ndigits - 1; i++) {
3911+
abs_val |= (uint64_t)v->long_value.ob_digit[i] << shift;
3912+
shift += PyLong_SHIFT;
3913+
}
3914+
uint64_t top = (uint64_t)v->long_value.ob_digit[ndigits - 1];
3915+
if (ndigits == _PY_LONG_MAX_DIGITS_FOR_INT64 && (top >> (64 - shift)) != 0) {
3916+
return false;
3917+
}
3918+
abs_val |= top << shift;
3919+
if (abs_val <= (uint64_t)INT64_MAX) {
3920+
*out = sign < 0 ? -(int64_t)abs_val : (int64_t)abs_val;
3921+
return true;
3922+
}
3923+
if (sign < 0 && abs_val == (uint64_t)INT64_MAX + 1) {
3924+
*out = INT64_MIN;
3925+
return true;
3926+
}
3927+
return false;
3928+
}
3929+
3930+
static inline bool
3931+
_Py_i64_add_overflow(int64_t a, int64_t b, int64_t *out)
3932+
{
3933+
#if defined(__GNUC__) || defined(__clang__)
3934+
return __builtin_add_overflow(a, b, out);
3935+
#else
3936+
if ((b > 0 && a > INT64_MAX - b) || (b < 0 && a < INT64_MIN - b)) {
3937+
return true;
3938+
}
3939+
*out = a + b;
3940+
return false;
3941+
#endif
3942+
}
3943+
3944+
static inline PyLongObject *
3945+
_wide_op_result(int64_t v)
3946+
{
3947+
if (IS_SMALL_INT(v)) {
3948+
return (PyLongObject *)Py_NewRef(get_small_int((sdigit)v));
3949+
}
3950+
assert(v != 0);
3951+
if (is_medium_int(v)) {
3952+
PyLongObject *result = (PyLongObject *)_Py_FREELIST_POP(PyLongObject, ints);
3953+
if (result == NULL) {
3954+
result = PyObject_Malloc(sizeof(PyLongObject));
3955+
if (result == NULL) {
3956+
return (PyLongObject *)PyErr_NoMemory();
3957+
}
3958+
_PyObject_Init((PyObject *)result, &PyLong_Type);
3959+
_PyLong_InitTag(result);
3960+
}
3961+
digit abs_v = v < 0 ? (digit)(-(sdigit)v) : (digit)(sdigit)v;
3962+
_PyLong_SetSignAndDigitCount(result, v < 0 ? -1 : 1, 1);
3963+
result->long_value.ob_digit[0] = abs_v;
3964+
return result;
3965+
}
3966+
return (PyLongObject *)_PyLong_FromLarge(v);
3967+
}
3968+
3969+
/* Exact int -> int64_t helper for the wide int fast path.
3970+
* Keeps the exact-type check local to this translation unit. */
3971+
static inline bool
3972+
_PyLong_CheckExactAndTryAsInt64(PyObject *op, int64_t *out)
3973+
{
3974+
return PyLong_CheckExact(op) &&
3975+
_PyLong_TryAsInt64ExactLocal((PyLongObject *)op, out);
3976+
}
3977+
3978+
/* Wide variant: operands are exact ints in the full int64 range (may be
3979+
* non-compact). Returns NULL without raising when an input is out of int64
3980+
* range or the sum overflows int64. */
3981+
static PyLongObject *
3982+
_PyCompactLong_AddWide(PyLongObject *a, PyLongObject *b)
3983+
{
3984+
if (_PyLong_BothAreCompact(a, b)) {
3985+
stwodigits v = medium_value(a) + medium_value(b);
3986+
return _PyLong_FromSTwoDigits(v);
3987+
}
3988+
int64_t va, vb;
3989+
if (!_PyLong_CheckExactAndTryAsInt64((PyObject *)a, &va) ||
3990+
!_PyLong_CheckExactAndTryAsInt64((PyObject *)b, &vb)) {
3991+
return NULL;
3992+
}
3993+
int64_t v;
3994+
if (_Py_i64_add_overflow(va, vb, &v)) {
3995+
return NULL;
3996+
}
3997+
return _wide_op_result(v);
3998+
}
3999+
38744000
static PyObject *
38754001
long_add_method(PyObject *a, PyObject *b)
38764002
{
@@ -3886,6 +4012,11 @@ long_sub(PyLongObject *a, PyLongObject *b)
38864012
return _PyLong_FromSTwoDigits(medium_value(a) - medium_value(b));
38874013
}
38884014

4015+
PyLongObject *fast = _PyCompactLong_SubtractWide(a, b);
4016+
if (fast != NULL || PyErr_Occurred()) {
4017+
return fast;
4018+
}
4019+
38894020
PyLongObject *z;
38904021
if (_PyLong_IsNegative(a)) {
38914022
if (_PyLong_IsNegative(b)) {
@@ -3916,6 +4047,39 @@ _PyCompactLong_Subtract(PyLongObject *a, PyLongObject *b)
39164047
return medium_from_stwodigits(v);
39174048
}
39184049

4050+
static inline bool
4051+
_Py_i64_sub_overflow(int64_t a, int64_t b, int64_t *out)
4052+
{
4053+
#if defined(__GNUC__) || defined(__clang__)
4054+
return __builtin_sub_overflow(a, b, out);
4055+
#else
4056+
if ((b < 0 && a > INT64_MAX + b) || (b > 0 && a < INT64_MIN + b)) {
4057+
return true;
4058+
}
4059+
*out = a - b;
4060+
return false;
4061+
#endif
4062+
}
4063+
4064+
static PyLongObject *
4065+
_PyCompactLong_SubtractWide(PyLongObject *a, PyLongObject *b)
4066+
{
4067+
if (_PyLong_BothAreCompact(a, b)) {
4068+
stwodigits v = medium_value(a) - medium_value(b);
4069+
return _PyLong_FromSTwoDigits(v);
4070+
}
4071+
int64_t va, vb;
4072+
if (!_PyLong_CheckExactAndTryAsInt64((PyObject *)a, &va) ||
4073+
!_PyLong_CheckExactAndTryAsInt64((PyObject *)b, &vb)) {
4074+
return NULL;
4075+
}
4076+
int64_t v;
4077+
if (_Py_i64_sub_overflow(va, vb, &v)) {
4078+
return NULL;
4079+
}
4080+
return _wide_op_result(v);
4081+
}
4082+
39194083
static PyObject *
39204084
long_sub_method(PyObject *a, PyObject *b)
39214085
{

0 commit comments

Comments
 (0)