Feature or enhancement
Proposal
tuple.index(), tuple.count(), and tuple membership currently use PyObject_RichCompareBool(..., Py_EQ) for every element examined, even when both the tuple element and the searched-for value are exact compact int objects.
For two exact compact integers, equality can be determined directly from _PyLong_CompactValue(), avoiding generic rich-comparison dispatch on every scanned element. Because these comparisons occur inside linear scans, the potential saving scales with tuple length.
Current implementation
On current main, tuple.index() contains:
for (i = start; i < stop; i++) {
int cmp = PyObject_RichCompareBool(self->ob_item[i], value, Py_EQ);
if (cmp > 0)
return PyLong_FromSsize_t(i);
else if (cmp < 0)
return NULL;
}
tuple.count() similarly calls:
PyObject_RichCompareBool(self->ob_item[i], value, Py_EQ)
for each element, and tuple_contains() uses the same generic rich-comparison operation.
Therefore code such as:
t = tuple(range(10_000))
9999 in t
t.index(9999)
t.count(-1)
can execute thousands of generic rich-comparison calls even though each comparison is between ordinary exact integers.
Possible fast path
pycore_long.h already provides _PyLong_CheckExactAndCompact() and _PyLong_CompactValue().
If the searched-for value is an exact compact integer, its C value could be extracted once before the scan:
bool compact_value = _PyLong_CheckExactAndCompact(value);
Py_ssize_t needle;
if (compact_value) {
needle = _PyLong_CompactValue((PyLongObject *)value);
}
The loop could then use direct comparison whenever the current tuple element is also an exact compact integer:
int cmp;
if (compact_value && _PyLong_CheckExactAndCompact(item)) {
cmp = (_PyLong_CompactValue((PyLongObject *)item) == needle);
}
else {
cmp = PyObject_RichCompareBool(item, value, Py_EQ);
}
Objects/tupleobject.c would need to include pycore_long.h.
The pattern could potentially be shared by:
tuple_contains()
tuple_index_impl()
tuple_count_impl()
An implementation could also branch once on whether value is an exact compact integer and use a separate loop for that case, to avoid adding an unnecessary per-iteration condition to the normal non-integer path.
Semantics
The shortcut would only be used when both operands are exact compact int objects.
For exact integers, CPython's existing integer comparison already compares compact values directly in long_compare(), so direct comparison of _PyLong_CompactValue() produces the same equality result.
No user-defined comparison method can be involved when both operands are exact int objects.
Cases involving integer subclasses or other numeric types would therefore continue through the existing generic path, for example:
class MyInt(int):
def __eq__(self, other):
...
t = (1, 2, MyInt(3))
Similarly, comparisons involving bool, float, Decimal, NumPy scalar types, or arbitrary objects would remain generic.
The optimization also cannot cause Python-level side effects while comparing two exact integers.
Why this may be worthwhile
The expensive operation being removed occurs once per examined tuple element.
For a tuple containing thousands of integers, thousands of calls to PyObject_RichCompareBool(...) could instead become a compact-int type check, value load, and machine equality comparison.
This makes the opportunity different from optimizations that save only a fixed amount of function-call overhead: the saving grows with the number of elements scanned.
The relevant workloads are:
value in tuple_of_ints
tuple_of_ints.index(value)
tuple_of_ints.count(value)
Benchmark
A patch should be benchmarked on current main with pyperf, including at least:
- membership hit near the end;
- membership miss;
index() hit near the end;
count() miss;
- several tuple sizes such as 10, 100, 1,000, and 10,000;
- non-integer tuples to check that the additional specialization does not regress the generic path.
For example:
t = tuple(range(10_000))
needle_hit = 9_999
needle_miss = -1
needle_hit in t
needle_miss in t
t.index(needle_hit)
t.count(needle_miss)
Possible follow-up
If this proves beneficial for tuples, similar exact-integer shortcuts might be worth investigating for list search operations.
Tuples seem like a good first target because their contents are immutable and the existing search loops are particularly simple.
Has this already been discussed elsewhere?
This is a minor performance enhancement which does not need previous discussion elsewhere.
Links to previous discussion of this feature
No response
Feature or enhancement
Proposal
tuple.index(),tuple.count(), and tuple membership currently usePyObject_RichCompareBool(..., Py_EQ)for every element examined, even when both the tuple element and the searched-for value are exact compactintobjects.For two exact compact integers, equality can be determined directly from
_PyLong_CompactValue(), avoiding generic rich-comparison dispatch on every scanned element. Because these comparisons occur inside linear scans, the potential saving scales with tuple length.Current implementation
On current
main,tuple.index()contains:tuple.count()similarly calls:for each element, and
tuple_contains()uses the same generic rich-comparison operation.Therefore code such as:
can execute thousands of generic rich-comparison calls even though each comparison is between ordinary exact integers.
Possible fast path
pycore_long.halready provides_PyLong_CheckExactAndCompact()and_PyLong_CompactValue().If the searched-for value is an exact compact integer, its C value could be extracted once before the scan:
The loop could then use direct comparison whenever the current tuple element is also an exact compact integer:
Objects/tupleobject.cwould need to includepycore_long.h.The pattern could potentially be shared by:
tuple_contains()tuple_index_impl()tuple_count_impl()An implementation could also branch once on whether
valueis an exact compact integer and use a separate loop for that case, to avoid adding an unnecessary per-iteration condition to the normal non-integer path.Semantics
The shortcut would only be used when both operands are exact compact
intobjects.For exact integers, CPython's existing integer comparison already compares compact values directly in
long_compare(), so direct comparison of_PyLong_CompactValue()produces the same equality result.No user-defined comparison method can be involved when both operands are exact
intobjects.Cases involving integer subclasses or other numeric types would therefore continue through the existing generic path, for example:
Similarly, comparisons involving
bool,float,Decimal, NumPy scalar types, or arbitrary objects would remain generic.The optimization also cannot cause Python-level side effects while comparing two exact integers.
Why this may be worthwhile
The expensive operation being removed occurs once per examined tuple element.
For a tuple containing thousands of integers, thousands of calls to
PyObject_RichCompareBool(...)could instead become a compact-int type check, value load, and machine equality comparison.This makes the opportunity different from optimizations that save only a fixed amount of function-call overhead: the saving grows with the number of elements scanned.
The relevant workloads are:
Benchmark
A patch should be benchmarked on current
mainwithpyperf, including at least:index()hit near the end;count()miss;For example:
Possible follow-up
If this proves beneficial for tuples, similar exact-integer shortcuts might be worth investigating for list search operations.
Tuples seem like a good first target because their contents are immutable and the existing search loops are particularly simple.
Has this already been discussed elsewhere?
This is a minor performance enhancement which does not need previous discussion elsewhere.
Links to previous discussion of this feature
No response