Skip to content

Speed up tuple search operations for exact compact integers #156711

Description

@lesshaste

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions