From 8dfd0f093beacf946673ec0339f70acdbc4197c2 Mon Sep 17 00:00:00 2001 From: Lucas Zhou Date: Fri, 9 Oct 2026 17:38:40 -0500 Subject: [PATCH 1/3] gh-159092: Optimize comparisons in list operations Use specialized comparisons for exact int, float and str objects to avoid unnecessary rich-comparison dispatch in list operations. --- Lib/test/test_list.py | 10 ++++ ...-10-09-17-39-15.gh-issue-159092.qdquS4.rst | 1 + Objects/listobject.c | 54 ++++++++++++++++--- 3 files changed, 59 insertions(+), 6 deletions(-) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2026-10-09-17-39-15.gh-issue-159092.qdquS4.rst diff --git a/Lib/test/test_list.py b/Lib/test/test_list.py index 260f9f4f607f152..6f37092a2f36e70 100644 --- a/Lib/test/test_list.py +++ b/Lib/test/test_list.py @@ -363,6 +363,16 @@ def __eq__(self, other): lst = [X(), X()] X() in lst + def test_eq_identity_shortcut(self): + # PyObject_RichCompareBool() treats identity as equality, even for + # objects that are not equal to themselves (see seq_tests). + nan = float("nan") + self.assertIn(nan, [nan]) + self.assertEqual([nan].index(nan), 0) + self.assertEqual([nan].count(nan), 1) + self.assertEqual([nan], [nan]) + self.assertNotEqual([nan], [float("nan")]) + def test_tier2_invalidates_iterator(self): # GH-121012 for _ in range(100): diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-10-09-17-39-15.gh-issue-159092.qdquS4.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-10-09-17-39-15.gh-issue-159092.qdquS4.rst new file mode 100644 index 000000000000000..4aa44e6a5750822 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-10-09-17-39-15.gh-issue-159092.qdquS4.rst @@ -0,0 +1 @@ +Speed up ``x in list``, :meth:`list.index`, :meth:`list.count`, :meth:`list.remove` and equality comparisons for lists of integers, floats and strings by specializing the per-item equality check. diff --git a/Objects/listobject.c b/Objects/listobject.c index f9488cc5c384bab..4e3fb306b41d7c4 100644 --- a/Objects/listobject.c +++ b/Objects/listobject.c @@ -16,6 +16,7 @@ #include "pycore_stackref.h" // _Py_TryIncrefCompareStackRef() #include "pycore_tuple.h" // _PyTuple_FromArraySteal() #include "pycore_typeobject.h" // _Py_TYPE_VERSION_LIST +#include "pycore_unicodeobject.h" // _PyUnicode_Equal() #include /*[clinic input] @@ -707,17 +708,55 @@ list_length(PyObject *a) return PyList_GET_SIZE(a); } +static inline PyTypeObject * +list_fast_eq_type(PyObject *value) +{ + if (value != NULL) { + PyTypeObject *type = Py_TYPE(value); + if (type == &PyLong_Type || type == &PyFloat_Type || + type == &PyUnicode_Type) { + return type; + } + } + return NULL; +} + +static inline int +list_item_eq(PyObject *item, PyObject *value, PyTypeObject *fast_type) +{ + if (fast_type != NULL && Py_TYPE(item) == fast_type) { + if (item == value) { + return 1; + } + if (fast_type == &PyLong_Type) { + PyLongObject *v = (PyLongObject *)item; + PyLongObject *w = (PyLongObject *)value; + if (_PyLong_BothAreCompact(v, w)) { + return _PyLong_CompactValue(v) == _PyLong_CompactValue(w); + } + } + else if (fast_type == &PyFloat_Type) { + return PyFloat_AS_DOUBLE(item) == PyFloat_AS_DOUBLE(value); + } + else { + assert(fast_type == &PyUnicode_Type); + return _PyUnicode_Equal(item, value); + } + } + return PyObject_RichCompareBool(item, value, Py_EQ); +} + static int list_contains(PyObject *aa, PyObject *el) { - + PyTypeObject *fast_type = list_fast_eq_type(el); for (Py_ssize_t i = 0; ; i++) { PyObject *item = list_get_item_ref((PyListObject *)aa, i); if (item == NULL) { // out-of-bounds return 0; } - int cmp = PyObject_RichCompareBool(item, el, Py_EQ); + int cmp = list_item_eq(item, el, fast_type); Py_DECREF(item); if (cmp != 0) { return cmp; @@ -3391,13 +3430,14 @@ list_index_impl(PyListObject *self, PyObject *value, Py_ssize_t start, if (stop < 0) stop = 0; } + PyTypeObject *fast_type = list_fast_eq_type(value); for (Py_ssize_t i = start; i < stop; i++) { PyObject *obj = list_get_item_ref(self, i); if (obj == NULL) { // out-of-bounds break; } - int cmp = PyObject_RichCompareBool(obj, value, Py_EQ); + int cmp = list_item_eq(obj, value, fast_type); Py_DECREF(obj); if (cmp > 0) return PyLong_FromSsize_t(i); @@ -3422,6 +3462,7 @@ list_count_impl(PyListObject *self, PyObject *value) /*[clinic end generated code: output=eff66f14aef2df86 input=3bdc3a5e6f749565]*/ { Py_ssize_t count = 0; + PyTypeObject *fast_type = list_fast_eq_type(value); for (Py_ssize_t i = 0; ; i++) { PyObject *obj = list_get_item_ref(self, i); if (obj == NULL) { @@ -3433,7 +3474,7 @@ list_count_impl(PyListObject *self, PyObject *value) Py_DECREF(obj); continue; } - int cmp = PyObject_RichCompareBool(obj, value, Py_EQ); + int cmp = list_item_eq(obj, value, fast_type); Py_DECREF(obj); if (cmp > 0) count++; @@ -3460,11 +3501,12 @@ list_remove_impl(PyListObject *self, PyObject *value) /*[clinic end generated code: output=b9b76a6633b18778 input=26c813dbb95aa93b]*/ { Py_ssize_t i; + PyTypeObject *fast_type = list_fast_eq_type(value); for (i = 0; i < Py_SIZE(self); i++) { PyObject *obj = self->ob_item[i]; Py_INCREF(obj); - int cmp = PyObject_RichCompareBool(obj, value, Py_EQ); + int cmp = list_item_eq(obj, value, fast_type); Py_DECREF(obj); if (cmp > 0) { if (list_ass_slice_lock_held(self, i, i+1, NULL) == 0) @@ -3522,7 +3564,7 @@ list_richcompare_impl(PyObject *v, PyObject *w, int op) Py_INCREF(vitem); Py_INCREF(witem); - int k = PyObject_RichCompareBool(vitem, witem, Py_EQ); + int k = list_item_eq(vitem, witem, list_fast_eq_type(witem)); if (k < 0) { Py_DECREF(vitem); Py_DECREF(witem); From c884032d6c52c0d43514a5e566f5f5df65956740 Mon Sep 17 00:00:00 2001 From: Lucas Zhou Date: Sat, 10 Oct 2026 16:12:13 -0500 Subject: [PATCH 2/3] gh-159092: Check inlined list comparisons in debug builds Assert that the int, float and str fast paths agree with PyObject_RichCompareBool(). --- Objects/listobject.c | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/Objects/listobject.c b/Objects/listobject.c index 4e3fb306b41d7c4..eca07a685a576cd 100644 --- a/Objects/listobject.c +++ b/Objects/listobject.c @@ -725,23 +725,27 @@ static inline int list_item_eq(PyObject *item, PyObject *value, PyTypeObject *fast_type) { if (fast_type != NULL && Py_TYPE(item) == fast_type) { + int res; if (item == value) { return 1; } if (fast_type == &PyLong_Type) { PyLongObject *v = (PyLongObject *)item; PyLongObject *w = (PyLongObject *)value; - if (_PyLong_BothAreCompact(v, w)) { - return _PyLong_CompactValue(v) == _PyLong_CompactValue(w); + if (!_PyLong_BothAreCompact(v, w)) { + return PyObject_RichCompareBool(item, value, Py_EQ); } + res = _PyLong_CompactValue(v) == _PyLong_CompactValue(w); } else if (fast_type == &PyFloat_Type) { - return PyFloat_AS_DOUBLE(item) == PyFloat_AS_DOUBLE(value); + res = PyFloat_AS_DOUBLE(item) == PyFloat_AS_DOUBLE(value); } else { assert(fast_type == &PyUnicode_Type); - return _PyUnicode_Equal(item, value); + res = _PyUnicode_Equal(item, value); } + assert(res == PyObject_RichCompareBool(item, value, Py_EQ)); + return res; } return PyObject_RichCompareBool(item, value, Py_EQ); } From 67bb2ef1b7977861d389114590d5855e38821f87 Mon Sep 17 00:00:00 2001 From: Lucas Zhou Date: Sun, 11 Oct 2026 16:44:06 -0500 Subject: [PATCH 3/3] gh-159092: Limit the list comparison fast path to list.__contains__ Keep the generic scan in a separate non-inlinable function so that PyObject_RichCompareBool() is still inlined with LTO and PGO; drop the fast paths for list.index(), count(), remove() and the rich comparison, which are much rarer operations and do not gain enough to be worth the extra code. --- Lib/test/test_list.py | 5 +-- ...-10-09-17-39-15.gh-issue-159092.qdquS4.rst | 2 +- Objects/listobject.c | 32 +++++++++++++++---- 3 files changed, 27 insertions(+), 12 deletions(-) diff --git a/Lib/test/test_list.py b/Lib/test/test_list.py index 6f37092a2f36e70..0d8f0a6c55e9f36 100644 --- a/Lib/test/test_list.py +++ b/Lib/test/test_list.py @@ -368,10 +368,7 @@ def test_eq_identity_shortcut(self): # objects that are not equal to themselves (see seq_tests). nan = float("nan") self.assertIn(nan, [nan]) - self.assertEqual([nan].index(nan), 0) - self.assertEqual([nan].count(nan), 1) - self.assertEqual([nan], [nan]) - self.assertNotEqual([nan], [float("nan")]) + self.assertNotIn(float("nan"), [nan]) def test_tier2_invalidates_iterator(self): # GH-121012 diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-10-09-17-39-15.gh-issue-159092.qdquS4.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-10-09-17-39-15.gh-issue-159092.qdquS4.rst index 4aa44e6a5750822..3bd5d7778620245 100644 --- a/Misc/NEWS.d/next/Core_and_Builtins/2026-10-09-17-39-15.gh-issue-159092.qdquS4.rst +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-10-09-17-39-15.gh-issue-159092.qdquS4.rst @@ -1 +1 @@ -Speed up ``x in list``, :meth:`list.index`, :meth:`list.count`, :meth:`list.remove` and equality comparisons for lists of integers, floats and strings by specializing the per-item equality check. +Speed up ``x in list`` for lists of integers, floats and strings by specializing the per-item equality check. diff --git a/Objects/listobject.c b/Objects/listobject.c index eca07a685a576cd..a99d9865572bcb0 100644 --- a/Objects/listobject.c +++ b/Objects/listobject.c @@ -750,10 +750,31 @@ list_item_eq(PyObject *item, PyObject *value, PyTypeObject *fast_type) return PyObject_RichCompareBool(item, value, Py_EQ); } +static Py_NO_INLINE int +list_contains_generic(PyListObject *a, PyObject *el) +{ + for (Py_ssize_t i = 0; ; i++) { + PyObject *item = list_get_item_ref(a, i); + if (item == NULL) { + // out-of-bounds + return 0; + } + int cmp = PyObject_RichCompareBool(item, el, Py_EQ); + Py_DECREF(item); + if (cmp != 0) { + return cmp; + } + } + return 0; +} + static int list_contains(PyObject *aa, PyObject *el) { PyTypeObject *fast_type = list_fast_eq_type(el); + if (fast_type == NULL) { + return list_contains_generic((PyListObject *)aa, el); + } for (Py_ssize_t i = 0; ; i++) { PyObject *item = list_get_item_ref((PyListObject *)aa, i); if (item == NULL) { @@ -3434,14 +3455,13 @@ list_index_impl(PyListObject *self, PyObject *value, Py_ssize_t start, if (stop < 0) stop = 0; } - PyTypeObject *fast_type = list_fast_eq_type(value); for (Py_ssize_t i = start; i < stop; i++) { PyObject *obj = list_get_item_ref(self, i); if (obj == NULL) { // out-of-bounds break; } - int cmp = list_item_eq(obj, value, fast_type); + int cmp = PyObject_RichCompareBool(obj, value, Py_EQ); Py_DECREF(obj); if (cmp > 0) return PyLong_FromSsize_t(i); @@ -3466,7 +3486,6 @@ list_count_impl(PyListObject *self, PyObject *value) /*[clinic end generated code: output=eff66f14aef2df86 input=3bdc3a5e6f749565]*/ { Py_ssize_t count = 0; - PyTypeObject *fast_type = list_fast_eq_type(value); for (Py_ssize_t i = 0; ; i++) { PyObject *obj = list_get_item_ref(self, i); if (obj == NULL) { @@ -3478,7 +3497,7 @@ list_count_impl(PyListObject *self, PyObject *value) Py_DECREF(obj); continue; } - int cmp = list_item_eq(obj, value, fast_type); + int cmp = PyObject_RichCompareBool(obj, value, Py_EQ); Py_DECREF(obj); if (cmp > 0) count++; @@ -3505,12 +3524,11 @@ list_remove_impl(PyListObject *self, PyObject *value) /*[clinic end generated code: output=b9b76a6633b18778 input=26c813dbb95aa93b]*/ { Py_ssize_t i; - PyTypeObject *fast_type = list_fast_eq_type(value); for (i = 0; i < Py_SIZE(self); i++) { PyObject *obj = self->ob_item[i]; Py_INCREF(obj); - int cmp = list_item_eq(obj, value, fast_type); + int cmp = PyObject_RichCompareBool(obj, value, Py_EQ); Py_DECREF(obj); if (cmp > 0) { if (list_ass_slice_lock_held(self, i, i+1, NULL) == 0) @@ -3568,7 +3586,7 @@ list_richcompare_impl(PyObject *v, PyObject *w, int op) Py_INCREF(vitem); Py_INCREF(witem); - int k = list_item_eq(vitem, witem, list_fast_eq_type(witem)); + int k = PyObject_RichCompareBool(vitem, witem, Py_EQ); if (k < 0) { Py_DECREF(vitem); Py_DECREF(witem);