Feature or enhancement
Proposal
tuple_richcompare() currently calls PyObject_RichCompareBool(..., Py_EQ) for each pair of corresponding tuple elements until it finds a mismatch.
When both elements are exact compact int objects, equality can be determined directly from _PyLong_CompactValue(), avoiding generic rich-comparison dispatch for every element in a long common prefix.
This could speed up comparisons such as:
a == b
a != b
a < b
a <= b
a > b
a >= b
when the tuples contain ordinary integers.
Current implementation
On current main, tuple_richcompare() searches for the first unequal pair with:
for (i = 0; i < vlen && i < wlen; i++) {
int k = PyObject_RichCompareBool(vt->ob_item[i],
wt->ob_item[i], Py_EQ);
if (k < 0)
return NULL;
if (!k)
break;
}
If a differing pair is found, == and != are resolved immediately. For the ordering operators, the differing elements are then compared again using:
return PyObject_RichCompare(vt->ob_item[i], wt->ob_item[i], op);
Consequently, comparing two equal integer tuples of length 10,000 can perform 10,000 generic rich-comparison operations even though each comparison is between ordinary integers.
Similarly, tuples with a long common integer prefix perform a generic comparison for every element in that prefix.
Possible fast path
pycore_long.h already provides _PyLong_CheckExactAndCompact() and _PyLong_CompactValue().
tuple_richcompare() could therefore special-case a pair when both objects are exact compact integers:
PyObject *vitem = vt->ob_item[i];
PyObject *witem = wt->ob_item[i];
if (vitem == witem) {
continue;
}
if (_PyLong_CheckExactAndCompact(vitem) &&
_PyLong_CheckExactAndCompact(witem))
{
Py_ssize_t a =
_PyLong_CompactValue((PyLongObject *)vitem);
Py_ssize_t b =
_PyLong_CompactValue((PyLongObject *)witem);
if (a == b) {
continue;
}
break;
}
int k = PyObject_RichCompareBool(vitem, witem, Py_EQ);
if (k < 0) {
return NULL;
}
if (!k) {
break;
}
Objects/tupleobject.c would need to include pycore_long.h.
The existing generic path would remain unchanged for every other type.
A further small optimization is possible once the first differing elements are known. If they are also exact compact integers, the ordering operation could be evaluated directly:
Py_RETURN_RICHCOMPARE(a, b, op);
instead of calling PyObject_RichCompare().
That second optimization is optional; avoiding generic equality comparisons throughout a long integer prefix is the main opportunity.
Correctness
The shortcut is restricted to exact int objects.
This is important because integer subclasses may override comparison methods:
class MyInt(int):
def __eq__(self, other):
...
Such objects would continue through PyObject_RichCompareBool() exactly as they do now.
Other numeric types including bool, float, Decimal, NumPy scalar types, etc. would also remain on the generic path.
For two exact compact integers, CPython's existing integer comparison already compares their compact values directly. long_compare() contains:
if (_PyLong_BothAreCompact(a, b)) {
return _PyLong_CompactValue(a) - _PyLong_CompactValue(b);
}
so comparing _PyLong_CompactValue() values directly gives the same equality and ordering result.
The current PyObject_RichCompareBool() also treats identical objects as equal before generic dispatch. An explicit:
if (vitem == witem)
continue;
preserves that behavior while avoiding even the compact-int tests in that case.
Tuple immutability means that the tuple contents and lengths cannot change as a side effect of comparing two exact integer elements. The existing implementation already explicitly relies on tuple immutability to reuse the cached tuple lengths across comparison calls.
Why this may be worthwhile
The avoided overhead occurs once per corresponding element, rather than once per Python operation.
For:
a = tuple(...)
b = tuple(...)
a == b
with a long common integer prefix, the current cost includes potentially thousands of calls to PyObject_RichCompareBool(...).
The exact compact-int path would replace each of those with a small number of inline checks, two compact-value loads, and a machine integer comparison.
This is particularly relevant for:
- equality of long integer tuples;
- inequality where the first mismatch occurs late;
- lexicographical ordering of tuples with long common prefixes;
- tuples used as structured numeric records or keys.
Benchmark
A patch should be benchmarked with pyperf on current main.
It is important that the benchmark include equal integer objects that are not merely identical references, since PyObject_RichCompareBool() already has a fast identity check.
For example:
import pyperf
runner = pyperf.Runner()
for n in (10, 100, 1_000, 10_000):
base = 100_000
# Construct separately so corresponding ints are equal
# but generally distinct Python objects.
a = tuple(range(base, base + n))
b = tuple(range(base, base + n))
c = tuple(range(base, base + n))
c = c[:-1] + (base + n + 1,)
runner.timeit(
f"equal integer tuples, n={n}",
stmt="a == b",
globals={"a": a, "b": b},
)
runner.timeit(
f"late unequal integer tuples, n={n}",
stmt="a == c",
globals={"a": a, "c": c},
)
runner.timeit(
f"ordered integer tuples, n={n}",
stmt="a < c",
globals={"a": a, "c": c},
)
Benchmarks should also include:
- tuples whose corresponding elements are identical objects, to make sure that case does not regress;
- tuples of strings or arbitrary objects, to check the generic path;
- mixed-type tuples;
- short tuples, where additional fast-path tests must not outweigh the saving.
Scope
I suggest starting with tuples because the implementation is small and tuple immutability makes the fast path particularly simple.
If the change proves worthwhile, a similar idea might later be investigated for list comparisons, where mutation and free-threading require more care.
Related issues
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
Feature or enhancement
Proposal
tuple_richcompare()currently callsPyObject_RichCompareBool(..., Py_EQ)for each pair of corresponding tuple elements until it finds a mismatch.When both elements are exact compact
intobjects, equality can be determined directly from_PyLong_CompactValue(), avoiding generic rich-comparison dispatch for every element in a long common prefix.This could speed up comparisons such as:
when the tuples contain ordinary integers.
Current implementation
On current
main,tuple_richcompare()searches for the first unequal pair with:If a differing pair is found,
==and!=are resolved immediately. For the ordering operators, the differing elements are then compared again using:Consequently, comparing two equal integer tuples of length 10,000 can perform 10,000 generic rich-comparison operations even though each comparison is between ordinary integers.
Similarly, tuples with a long common integer prefix perform a generic comparison for every element in that prefix.
Possible fast path
pycore_long.halready provides_PyLong_CheckExactAndCompact()and_PyLong_CompactValue().tuple_richcompare()could therefore special-case a pair when both objects are exact compact integers:Objects/tupleobject.cwould need to includepycore_long.h.The existing generic path would remain unchanged for every other type.
A further small optimization is possible once the first differing elements are known. If they are also exact compact integers, the ordering operation could be evaluated directly:
instead of calling
PyObject_RichCompare().That second optimization is optional; avoiding generic equality comparisons throughout a long integer prefix is the main opportunity.
Correctness
The shortcut is restricted to exact
intobjects.This is important because integer subclasses may override comparison methods:
Such objects would continue through
PyObject_RichCompareBool()exactly as they do now.Other numeric types including
bool,float,Decimal, NumPy scalar types, etc. would also remain on the generic path.For two exact compact integers, CPython's existing integer comparison already compares their compact values directly.
long_compare()contains:so comparing
_PyLong_CompactValue()values directly gives the same equality and ordering result.The current
PyObject_RichCompareBool()also treats identical objects as equal before generic dispatch. An explicit:preserves that behavior while avoiding even the compact-int tests in that case.
Tuple immutability means that the tuple contents and lengths cannot change as a side effect of comparing two exact integer elements. The existing implementation already explicitly relies on tuple immutability to reuse the cached tuple lengths across comparison calls.
Why this may be worthwhile
The avoided overhead occurs once per corresponding element, rather than once per Python operation.
For:
with a long common integer prefix, the current cost includes potentially thousands of calls to
PyObject_RichCompareBool(...).The exact compact-int path would replace each of those with a small number of inline checks, two compact-value loads, and a machine integer comparison.
This is particularly relevant for:
Benchmark
A patch should be benchmarked with
pyperfon currentmain.It is important that the benchmark include equal integer objects that are not merely identical references, since
PyObject_RichCompareBool()already has a fast identity check.For example:
Benchmarks should also include:
Scope
I suggest starting with tuples because the implementation is small and tuple immutability makes the fast path particularly simple.
If the change proves worthwhile, a similar idea might later be investigated for list comparisons, where mutation and free-threading require more care.
Related issues
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