From 7f8a232682d5e6ab5701d7c74829076108aef8f6 Mon Sep 17 00:00:00 2001 From: jeffchen006 Date: Sat, 8 Aug 2026 15:55:38 +0000 Subject: [PATCH] gh-155385: Speed up range.__contains__ for compact ints Add a fast path to range_contains_long() for the common case where the needle and r->start / r->stop / r->step are all compact ints, doing the bounds and divisibility checks in Py_ssize_t instead of going through PyObject_RichCompareBool(), PyNumber_Subtract() and PyNumber_Remainder(). Compact ints hold a single digit, so their values are bounded by PyLong_MASK and "value - start" cannot overflow Py_ssize_t. C and Python remainders differ in sign but never in whether they are zero, so the divisibility test is unchanged. Anything outside the compact domain falls through to the previous implementation, now range_contains_long_slow(). This also speeds up range.count(), which shares the same helper. --- ...-08-08-15-55-16.gh-issue-155385.7DdKrH.rst | 4 ++ Objects/rangeobject.c | 58 ++++++++++++++++++- 2 files changed, 61 insertions(+), 1 deletion(-) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2026-08-08-15-55-16.gh-issue-155385.7DdKrH.rst 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) {