gh-155385: Speed up range.__contains__ for compact ints - #155386
Draft
jeffchen006 wants to merge 1 commit into
Draft
gh-155385: Speed up range.__contains__ for compact ints#155386jeffchen006 wants to merge 1 commit into
range.__contains__ for compact ints#155386jeffchen006 wants to merge 1 commit into
Conversation
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
range.__contains__andrange.count()route every exact-intorboolneedle through
range_contains_long(), which performs the whole membershiptest through the abstract number API: up to three
PyObject_RichCompareBool()calls, a
PyNumber_Subtract(), aPyNumber_Remainder(), and a finalPyObject_RichCompareBool(), allocating two intermediatePyLongObjects alongthe way.
That generality is only needed when an operand does not fit in a machine word.
This PR adds a fast path for the case where the needle and
r->start/r->stop/r->stepare all compact ints, doing the bounds check and thedivisibility check in
Py_ssize_t. Anything outside that domain falls throughto the previous implementation, renamed
range_contains_long_slow()andotherwise untouched.
Why it is safe
|v| <= PyLong_MASK(
2**30 - 1on 64-bit builds). Hence|value - start| <= 2**31 - 2, whichfits
Py_ssize_twith room to spare — on 15-bit-digit and 32-bit builds too.%and Python's%differ in the sign ofa non-zero result but never in whether the result is zero, so
== 0isequivalent to the
PyNumber_Remainder(...) == 0it replaces.stepis never 0, rejected byvalidate_step()at construction, so theremainder is always defined.
make_range_object()is the only placestart/stop/stepare assigned, and every caller supplies either aPyNumber_Index()result orPyNumber_*arithmetic over ints.range_contains()still sends them to_PySequence_IterSearch()before this code is reached.Testing
test_range,test_list,test_tuple,test_bytes,test_str,test_dict,test_index,test_intandtest_longall pass (run=902), as does-R 3:3 test_rangeon a--with-pydebugbuild.Beyond the suite, I diffed patched against unpatched output over ~125k
membership evaluations, with zero divergences:
start, stop ∈ [-8, 8],step ∈ {±1, ±2, ±3, ±4},needle ∈ [-12, 12], cross-checked against materialisedlist(r)forin,.index()and.count();start/stopdrawn from{0, ±1, ±2, ±3, ±7, ±(2**30 - 2), ±(2**30 - 1), ±2**30, ±(2**30 + 1), ±2**31, ±10**30, ±10**100}and
step ∈ {±1, ±3, ±(2**30 - 1), ±2**30, 10**40}, which straddles thecompact/non-compact boundary in each field independently;
(
float,intsubclass,complex, custom__eq__).Benchmarks
--enable-optimizations=no --with-pydebug=no CFLAGS="-O3 -g0", gcc 13.3,x86-64 Linux, otherwise-idle machine. Baseline and patched interpreters were
interleaved round by round, 120 samples per arm; figures are
minper-loop ns.The last row is a no-op control (a bare name load) included to show the harness
is unbiased.
999 in range(0, 1000, 3)998 in range(0, 1000, 3)(miss)997 in range(1000, 0, -3)True in range(0, 1000, 3)range(0, 1000, 3).count(999)10**100 in range(0, 1000, 3)(fallback)999 in range(0, 10**100, 3)(fallback)6.0 in range(0, 8, 3)(non-int)needleEvery fallback path is neutral within noise.
objdumpconfirms the helper isfully inlined into
range_contains_long()— four tag tests and integerarithmetic, no calls at all — while the fallback costs one load, one compare
and one branch before reaching the original code.
range.__contains__andrange.count()for compact ints #155385