diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-08-08-15-55-16.gh-issue-155385.7DdKrH.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-08-08-15-55-16.gh-issue-155385.7DdKrH.rst new file mode 100644 index 00000000000000..4f8524cd8d1cfa --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-08-08-15-55-16.gh-issue-155385.7DdKrH.rst @@ -0,0 +1,4 @@ +Speed up ``in`` and :meth:`!count` on :class:`range` objects by up to 4x when +the value and the range bounds are all small integers, by testing membership +with machine arithmetic instead of the abstract number API. Larger values fall +back to the previous implementation. diff --git a/Objects/rangeobject.c b/Objects/rangeobject.c index 55b7f108730728..047af426c42c05 100644 --- a/Objects/rangeobject.c +++ b/Objects/rangeobject.c @@ -454,9 +454,54 @@ compute_slice(rangeobject *r, PyObject *_slice) return NULL; } +/* Fast path for the common case where the needle and all three range fields + are compact ints, so that the whole membership test can be done in + Py_ssize_t arithmetic instead of going through the abstract number API. + + Compact ints hold a single digit, so their values are bounded by + PyLong_MASK (2**30 - 1 on 64-bit builds, 2**15 - 1 on 15-bit-digit builds). + That leaves at least one spare bit, so "value - start" cannot overflow + Py_ssize_t. + + Assumes (PyLong_CheckExact(ob) || PyBool_Check(ob)). Returns 1 or 0 like + range_contains_long(), or -1 to mean "not representable, use the general + path" -- this helper never raises, so -1 is not an error indicator here. */ +static int +range_contains_compact_long(rangeobject *r, PyObject *ob) +{ + if (!_PyLong_IsCompact((PyLongObject *)ob) || + !_PyLong_IsCompact((PyLongObject *)r->start) || + !_PyLong_IsCompact((PyLongObject *)r->stop) || + !_PyLong_IsCompact((PyLongObject *)r->step)) + { + return -1; + } + + Py_ssize_t value = _PyLong_CompactValue((PyLongObject *)ob); + Py_ssize_t start = _PyLong_CompactValue((PyLongObject *)r->start); + Py_ssize_t stop = _PyLong_CompactValue((PyLongObject *)r->stop); + Py_ssize_t step = _PyLong_CompactValue((PyLongObject *)r->step); + + if (step > 0) { + /* positive steps: start <= ob < stop */ + if (value < start || value >= stop) { + return 0; + } + } + else { + /* negative steps: stop < ob <= start */ + if (value > start || value <= stop) { + return 0; + } + } + /* C and Python remainders differ in sign but never in whether they are + zero, so this matches the (ob - start) % step == 0 test below. */ + return ((value - start) % step) == 0; +} + /* Assumes (PyLong_CheckExact(ob) || PyBool_Check(ob)) */ static int -range_contains_long(rangeobject *r, PyObject *ob) +range_contains_long_slow(rangeobject *r, PyObject *ob) { PyObject *zero = _PyLong_GetZero(); // borrowed reference int cmp1, cmp2, cmp3; @@ -500,6 +545,17 @@ range_contains_long(rangeobject *r, PyObject *ob) return result; } +/* Assumes (PyLong_CheckExact(ob) || PyBool_Check(ob)) */ +static int +range_contains_long(rangeobject *r, PyObject *ob) +{ + int result = range_contains_compact_long(r, ob); + if (result != -1) { + return result; + } + return range_contains_long_slow(r, ob); +} + static int range_contains(PyObject *self, PyObject *ob) {