From e834d59520ab1bbb235e7067f288d2e41a296d95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maurycy=20Paw=C5=82owski-Wiero=C5=84ski?= <5383+maurycy@users.noreply.github.com> Date: Wed, 26 Nov 2025 03:44:18 +0100 Subject: [PATCH 01/11] early return in tuple, direct comparison --- Objects/tupleobject.c | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/Objects/tupleobject.c b/Objects/tupleobject.c index 169ac69701da11..f7f540a904a065 100644 --- a/Objects/tupleobject.c +++ b/Objects/tupleobject.c @@ -666,24 +666,33 @@ tuple_richcompare(PyObject *v, PyObject *w, int op) if (!PyTuple_Check(v) || !PyTuple_Check(w)) Py_RETURN_NOTIMPLEMENTED; + if (v == w) { + Py_RETURN_RICHCOMPARE(0, 0, op); + } + vt = (PyTupleObject *)v; wt = (PyTupleObject *)w; vlen = Py_SIZE(vt); wlen = Py_SIZE(wt); - /* Note: the corresponding code for lists has an "early out" test - * here when op is EQ or NE and the lengths differ. That pays there, - * but Tim was unable to find any real code where EQ/NE tuple - * compares don't have the same length, so testing for it here would - * have cost without benefit. - */ + if (vlen != wlen && (op == Py_EQ || op == Py_NE)) { + if (op == Py_EQ) { + Py_RETURN_FALSE; + } + else { + Py_RETURN_TRUE; + } + } /* Search for the first index where items are different. * Note that because tuples are immutable, it's safe to reuse * vlen and wlen across the comparison calls. */ for (i = 0; i < vlen && i < wlen; i++) { + if (vt->ob_item[i] == wt->ob_item[i]) { + continue; + } int k = PyObject_RichCompareBool(vt->ob_item[i], wt->ob_item[i], Py_EQ); if (k < 0) From 956fef1c75a84aa1f87ba94033b1d1f0a30bbbc1 Mon Sep 17 00:00:00 2001 From: maurycy <5383+maurycy@users.noreply.github.com> Date: Wed, 26 Nov 2025 21:58:40 +0100 Subject: [PATCH 02/11] see how this impacts the benchmarks --- Objects/tupleobject.c | 3 --- 1 file changed, 3 deletions(-) diff --git a/Objects/tupleobject.c b/Objects/tupleobject.c index f7f540a904a065..679e20be78d2f1 100644 --- a/Objects/tupleobject.c +++ b/Objects/tupleobject.c @@ -690,9 +690,6 @@ tuple_richcompare(PyObject *v, PyObject *w, int op) * vlen and wlen across the comparison calls. */ for (i = 0; i < vlen && i < wlen; i++) { - if (vt->ob_item[i] == wt->ob_item[i]) { - continue; - } int k = PyObject_RichCompareBool(vt->ob_item[i], wt->ob_item[i], Py_EQ); if (k < 0) From 9ba81106b1d2b705166c555564a14ec65a252071 Mon Sep 17 00:00:00 2001 From: maurycy <5383+maurycy@users.noreply.github.com> Date: Thu, 27 Nov 2025 02:17:03 +0100 Subject: [PATCH 03/11] simpler branch --- Objects/tupleobject.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Objects/tupleobject.c b/Objects/tupleobject.c index 679e20be78d2f1..a4fda75b0b117b 100644 --- a/Objects/tupleobject.c +++ b/Objects/tupleobject.c @@ -676,11 +676,11 @@ tuple_richcompare(PyObject *v, PyObject *w, int op) vlen = Py_SIZE(vt); wlen = Py_SIZE(wt); - if (vlen != wlen && (op == Py_EQ || op == Py_NE)) { - if (op == Py_EQ) { + if (vlen != wlen) { + switch (op) { + case Py_EQ: Py_RETURN_FALSE; - } - else { + case Py_NE: Py_RETURN_TRUE; } } From 25d53ebaea394b7e53b4d404f91c16b098a9ed41 Mon Sep 17 00:00:00 2001 From: maurycy <5383+maurycy@users.noreply.github.com> Date: Thu, 27 Nov 2025 12:59:23 +0100 Subject: [PATCH 04/11] leave only the early out --- Objects/tupleobject.c | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Objects/tupleobject.c b/Objects/tupleobject.c index a4fda75b0b117b..ede596d794cb5d 100644 --- a/Objects/tupleobject.c +++ b/Objects/tupleobject.c @@ -666,10 +666,6 @@ tuple_richcompare(PyObject *v, PyObject *w, int op) if (!PyTuple_Check(v) || !PyTuple_Check(w)) Py_RETURN_NOTIMPLEMENTED; - if (v == w) { - Py_RETURN_RICHCOMPARE(0, 0, op); - } - vt = (PyTupleObject *)v; wt = (PyTupleObject *)w; From 08134481fb20b94617a82ad9460ff6176a01a044 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maurycy=20Paw=C5=82owski-Wiero=C5=84ski?= <5383+maurycy@users.noreply.github.com> Date: Thu, 27 Nov 2025 20:38:04 +0100 Subject: [PATCH 05/11] Revert "leave only the early out" This reverts commit 25d53ebaea394b7e53b4d404f91c16b098a9ed41. --- Objects/tupleobject.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Objects/tupleobject.c b/Objects/tupleobject.c index ede596d794cb5d..a4fda75b0b117b 100644 --- a/Objects/tupleobject.c +++ b/Objects/tupleobject.c @@ -666,6 +666,10 @@ tuple_richcompare(PyObject *v, PyObject *w, int op) if (!PyTuple_Check(v) || !PyTuple_Check(w)) Py_RETURN_NOTIMPLEMENTED; + if (v == w) { + Py_RETURN_RICHCOMPARE(0, 0, op); + } + vt = (PyTupleObject *)v; wt = (PyTupleObject *)w; From 440a23ad397c6ebcb02e62cd658b6d50663d4ee4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maurycy=20Paw=C5=82owski-Wiero=C5=84ski?= <5383+maurycy@users.noreply.github.com> Date: Fri, 28 Nov 2025 21:15:50 +0100 Subject: [PATCH 06/11] compare uncomparable --- Lib/test/seq_tests.py | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/Lib/test/seq_tests.py b/Lib/test/seq_tests.py index a41970d8f3f55a..14836e2982e95b 100644 --- a/Lib/test/seq_tests.py +++ b/Lib/test/seq_tests.py @@ -250,6 +250,43 @@ def __eq__(self, other): checklast = self.type2test([StopCompares(), 1]) self.assertRaises(DoNotTestEq, checklast.__contains__, 1) + def test_compare_nan(self): + nan = float('nan') + a = self.type2test([nan]) + b = self.type2test([nan]) + self.assertTrue(a == a) + self.assertFalse(a != a) + self.assertTrue(a == b) + self.assertFalse(a != b) + self.assertFalse(a < a) + self.assertFalse(a < b) + self.assertFalse(a > b) + + def test_compare_decimal_nan(self): + import decimal + nan = decimal.Decimal("NaN") + a = self.type2test([nan]) + b = self.type2test([nan]) + self.assertTrue(a == a) + self.assertFalse(a != a) + self.assertTrue(a == b) + self.assertFalse(a != b) + self.assertFalse(a < a) + self.assertFalse(a < b) + self.assertFalse(a > b) + + def test_compare_signed_zero(self): + a = self.type2test([0.0]) + b = self.type2test([-0.0]) + self.assertTrue(a == a) + self.assertFalse(a != a) + self.assertTrue(b == b) + self.assertFalse(b != b) + self.assertTrue(a == b) + self.assertFalse(a != b) + self.assertFalse(a > b) + self.assertFalse(a < b) + def test_len(self): self.assertEqual(len(self.type2test()), 0) self.assertEqual(len(self.type2test([])), 0) From c90153940b50fbd51786c3f963162fa858854bc7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maurycy=20Paw=C5=82owski-Wiero=C5=84ski?= <5383+maurycy@users.noreply.github.com> Date: Fri, 28 Nov 2025 21:36:06 +0100 Subject: [PATCH 07/11] " --- Lib/test/seq_tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/seq_tests.py b/Lib/test/seq_tests.py index 14836e2982e95b..1f13f70a2200c4 100644 --- a/Lib/test/seq_tests.py +++ b/Lib/test/seq_tests.py @@ -251,7 +251,7 @@ def __eq__(self, other): self.assertRaises(DoNotTestEq, checklast.__contains__, 1) def test_compare_nan(self): - nan = float('nan') + nan = float("nan") a = self.type2test([nan]) b = self.type2test([nan]) self.assertTrue(a == a) From 3f824c03e8ca8da1786c24d529429c9068758c7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maurycy=20Paw=C5=82owski-Wiero=C5=84ski?= <5383+maurycy@users.noreply.github.com> Date: Tue, 2 Dec 2025 12:21:39 +0100 Subject: [PATCH 08/11] check_compare_id --- Lib/test/seq_tests.py | 26 +++++++++----------------- 1 file changed, 9 insertions(+), 17 deletions(-) diff --git a/Lib/test/seq_tests.py b/Lib/test/seq_tests.py index 1f13f70a2200c4..dfc7bb0511a239 100644 --- a/Lib/test/seq_tests.py +++ b/Lib/test/seq_tests.py @@ -250,10 +250,7 @@ def __eq__(self, other): checklast = self.type2test([StopCompares(), 1]) self.assertRaises(DoNotTestEq, checklast.__contains__, 1) - def test_compare_nan(self): - nan = float("nan") - a = self.type2test([nan]) - b = self.type2test([nan]) + def check_compare_id(self, a, b): self.assertTrue(a == a) self.assertFalse(a != a) self.assertTrue(a == b) @@ -262,30 +259,25 @@ def test_compare_nan(self): self.assertFalse(a < b) self.assertFalse(a > b) + def test_compare_nan(self): + nan = float("nan") + a = self.type2test([nan]) + b = self.type2test([nan]) + self.check_compare_id(a, b) + def test_compare_decimal_nan(self): import decimal nan = decimal.Decimal("NaN") a = self.type2test([nan]) b = self.type2test([nan]) - self.assertTrue(a == a) - self.assertFalse(a != a) - self.assertTrue(a == b) - self.assertFalse(a != b) - self.assertFalse(a < a) - self.assertFalse(a < b) - self.assertFalse(a > b) + self.check_compare_id(a, b) def test_compare_signed_zero(self): a = self.type2test([0.0]) b = self.type2test([-0.0]) - self.assertTrue(a == a) - self.assertFalse(a != a) + self.check_compare_id(a, b) self.assertTrue(b == b) self.assertFalse(b != b) - self.assertTrue(a == b) - self.assertFalse(a != b) - self.assertFalse(a > b) - self.assertFalse(a < b) def test_len(self): self.assertEqual(len(self.type2test()), 0) From c703f61c450fa2f1df02d4d5ab56b9709ead5db4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maurycy=20Paw=C5=82owski-Wiero=C5=84ski?= <5383+maurycy@users.noreply.github.com> Date: Tue, 2 Dec 2025 12:52:57 +0100 Subject: [PATCH 09/11] exhaust check_compare_id --- Lib/test/seq_tests.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Lib/test/seq_tests.py b/Lib/test/seq_tests.py index dfc7bb0511a239..ce1c515ccabe08 100644 --- a/Lib/test/seq_tests.py +++ b/Lib/test/seq_tests.py @@ -253,11 +253,16 @@ def __eq__(self, other): def check_compare_id(self, a, b): self.assertTrue(a == a) self.assertFalse(a != a) + self.assertTrue(b == b) + self.assertFalse(b != b) self.assertTrue(a == b) self.assertFalse(a != b) self.assertFalse(a < a) + self.assertFalse(b < b) self.assertFalse(a < b) self.assertFalse(a > b) + self.assertFalse(b < a) + self.assertFalse(b > a) def test_compare_nan(self): nan = float("nan") @@ -276,8 +281,6 @@ def test_compare_signed_zero(self): a = self.type2test([0.0]) b = self.type2test([-0.0]) self.check_compare_id(a, b) - self.assertTrue(b == b) - self.assertFalse(b != b) def test_len(self): self.assertEqual(len(self.type2test()), 0) From b1c53b2fd7c01979a63c4cb1a78229500cf79f71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maurycy=20Paw=C5=82owski-Wiero=C5=84ski?= <5383+maurycy@users.noreply.github.com> Date: Tue, 2 Dec 2025 13:27:51 +0100 Subject: [PATCH 10/11] comment --- Objects/tupleobject.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Objects/tupleobject.c b/Objects/tupleobject.c index a4fda75b0b117b..816e00bc4269d4 100644 --- a/Objects/tupleobject.c +++ b/Objects/tupleobject.c @@ -666,6 +666,11 @@ tuple_richcompare(PyObject *v, PyObject *w, int op) if (!PyTuple_Check(v) || !PyTuple_Check(w)) Py_RETURN_NOTIMPLEMENTED; + /* Fast path based on identity: if both objects are the same tuple + * object, we return immediately without comparing items. Elements that + * are not equal to themselves (see check_compare_id in + * Lib/tests/seq_tests.py) are therefore treated as equal here. + */ if (v == w) { Py_RETURN_RICHCOMPARE(0, 0, op); } From 0eae50d7b48ddc0a19834642ff29e221f8737f4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maurycy=20Paw=C5=82owski-Wiero=C5=84ski?= Date: Tue, 2 Dec 2025 13:33:43 +0100 Subject: [PATCH 11/11] Update Objects/tupleobject.c Co-authored-by: Victor Stinner --- Objects/tupleobject.c | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Objects/tupleobject.c b/Objects/tupleobject.c index 816e00bc4269d4..397ec1ca6d6198 100644 --- a/Objects/tupleobject.c +++ b/Objects/tupleobject.c @@ -668,8 +668,7 @@ tuple_richcompare(PyObject *v, PyObject *w, int op) /* Fast path based on identity: if both objects are the same tuple * object, we return immediately without comparing items. Elements that - * are not equal to themselves (see check_compare_id in - * Lib/tests/seq_tests.py) are therefore treated as equal here. + * are not equal to themselves are therefore treated as equal here. */ if (v == w) { Py_RETURN_RICHCOMPARE(0, 0, op);