From 22945429655ef17821a0f85611e467f87d9c404b Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Thu, 19 Feb 2026 12:46:53 +0100 Subject: [PATCH 1/8] gh-144995: Optimize memoryview == memoryview --- Lib/test/test_memoryview.py | 21 +++++++++++++++ ...-02-19-12-49-15.gh-issue-144995.Ob2oYJ.rst | 2 ++ Objects/memoryobject.c | 27 +++++++++++++++++++ 3 files changed, 50 insertions(+) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2026-02-19-12-49-15.gh-issue-144995.Ob2oYJ.rst diff --git a/Lib/test/test_memoryview.py b/Lib/test/test_memoryview.py index 656318668e6d6e..5aa2b0ecd6767d 100644 --- a/Lib/test/test_memoryview.py +++ b/Lib/test/test_memoryview.py @@ -575,6 +575,27 @@ def test_array_assign(self): m[:] = new_a self.assertEqual(a, new_a) + def test_compare_equal(self): + # A memoryview is equal to itself: there is no need to compare + # individual values. This is not true for float values since they can + # be NaN, and NaN is not equal to itself. + for int_format in 'bBhHiIlLqQ': + with self.subTest(format=int_format): + a = array.array(int_format, [1, 2, 3]) + m = memoryview(a) + self.assertTrue(m == m) + + for float_format in 'fd': + with self.subTest(format=int_format): + a = array.array(float_format, [1.0, 2.0, float('nan')]) + m = memoryview(a) + # nan is not equal to nan + self.assertFalse(m == m) + + a = array.array(float_format, [1.0, 2.0, 3.0]) + m = memoryview(a) + self.assertTrue(m == m) + class BytesMemorySliceTest(unittest.TestCase, BaseMemorySliceTests, BaseBytesMemoryTests): diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-02-19-12-49-15.gh-issue-144995.Ob2oYJ.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-02-19-12-49-15.gh-issue-144995.Ob2oYJ.rst new file mode 100644 index 00000000000000..83d84b9505c5a5 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-02-19-12-49-15.gh-issue-144995.Ob2oYJ.rst @@ -0,0 +1,2 @@ +Optimize :class:`memoryview` comparison: a :class:`memoryview` is equal to +itself, there is no need to compare values. Patch by Victor Stinner. diff --git a/Objects/memoryobject.c b/Objects/memoryobject.c index f3b7e4a396b4a1..1f8da86e35f4c1 100644 --- a/Objects/memoryobject.c +++ b/Objects/memoryobject.c @@ -3101,6 +3101,25 @@ cmp_rec(const char *p, const char *q, return 1; } +static int +is_float_format(const char *format) +{ + if (format == NULL) { + return 0; + } + if (strcmp("d", format) == 0) { + return 1; + } + if (strcmp("f", format) == 0) { + return 1; + } + if (strcmp("e", format) == 0) { + return 1; + } + return 0; +} + + static PyObject * memory_richcompare(PyObject *v, PyObject *w, int op) { @@ -3122,6 +3141,14 @@ memory_richcompare(PyObject *v, PyObject *w, int op) } vv = VIEW_ADDR(v); + // A memoryview is equal to itself: there is no need to compare individual + // values. This is not true for float values since they can be NaN, and NaN + // is not equal to itself. + if (v == w && !is_float_format(vv->format)) { + equal = 1; + goto result; + } + if (PyMemoryView_Check(w)) { if (BASE_INACCESSIBLE(w)) { equal = (v == w); From 0dd791196b64ec3dde61edbdb15cf4d320519c66 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Thu, 19 Feb 2026 16:45:53 +0100 Subject: [PATCH 2/8] Replace blocklist with allowlist --- Objects/memoryobject.c | 41 ++++++++++++++++++----------------------- 1 file changed, 18 insertions(+), 23 deletions(-) diff --git a/Objects/memoryobject.c b/Objects/memoryobject.c index 1f8da86e35f4c1..8ba75b51780fff 100644 --- a/Objects/memoryobject.c +++ b/Objects/memoryobject.c @@ -3101,25 +3101,6 @@ cmp_rec(const char *p, const char *q, return 1; } -static int -is_float_format(const char *format) -{ - if (format == NULL) { - return 0; - } - if (strcmp("d", format) == 0) { - return 1; - } - if (strcmp("f", format) == 0) { - return 1; - } - if (strcmp("e", format) == 0) { - return 1; - } - return 0; -} - - static PyObject * memory_richcompare(PyObject *v, PyObject *w, int op) { @@ -3143,10 +3124,24 @@ memory_richcompare(PyObject *v, PyObject *w, int op) // A memoryview is equal to itself: there is no need to compare individual // values. This is not true for float values since they can be NaN, and NaN - // is not equal to itself. - if (v == w && !is_float_format(vv->format)) { - equal = 1; - goto result; + // is not equal to itself. So only use this optimization on format known to + // not use floats. + if (v == w) { + int can_compare_ptrs; + const char *format = vv->format; + if (format != NULL) { + // Exclude formats "d" (double), "f" (float), "e" (16-bit float) + // and "P" (void*) + can_compare_ptrs = (strchr("bBchHiIlLnNqQ?", format[0]) != NULL + && format[1] == 0); + } + else { + can_compare_ptrs = 1; + } + if (can_compare_ptrs) { + equal = 1; + goto result; + } } if (PyMemoryView_Check(w)) { From 61e37e485f0846f28b0bcab939aea22b791138e5 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Thu, 19 Feb 2026 16:51:48 +0100 Subject: [PATCH 3/8] Dummy change to update GitHub --- Objects/memoryobject.c | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Objects/memoryobject.c b/Objects/memoryobject.c index 8ba75b51780fff..def2f57c14baac 100644 --- a/Objects/memoryobject.c +++ b/Objects/memoryobject.c @@ -3127,18 +3127,18 @@ memory_richcompare(PyObject *v, PyObject *w, int op) // is not equal to itself. So only use this optimization on format known to // not use floats. if (v == w) { - int can_compare_ptrs; + int can_compare_ptr; const char *format = vv->format; if (format != NULL) { // Exclude formats "d" (double), "f" (float), "e" (16-bit float) // and "P" (void*) - can_compare_ptrs = (strchr("bBchHiIlLnNqQ?", format[0]) != NULL - && format[1] == 0); + can_compare_ptr = (strchr("bBchHiIlLnNqQ?", format[0]) != NULL + && format[1] == 0); } else { - can_compare_ptrs = 1; + can_compare_ptr = 1; } - if (can_compare_ptrs) { + if (can_compare_ptr) { equal = 1; goto result; } From 102f26d4e963cfd6a06a5ee1a9104ce0446fb9af Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Thu, 19 Feb 2026 23:26:09 +0100 Subject: [PATCH 4/8] Apply suggestions from code review Co-authored-by: Pieter Eendebak --- Objects/memoryobject.c | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/Objects/memoryobject.c b/Objects/memoryobject.c index def2f57c14baac..76ac3f4d059c60 100644 --- a/Objects/memoryobject.c +++ b/Objects/memoryobject.c @@ -3122,16 +3122,17 @@ memory_richcompare(PyObject *v, PyObject *w, int op) } vv = VIEW_ADDR(v); - // A memoryview is equal to itself: there is no need to compare individual - // values. This is not true for float values since they can be NaN, and NaN - // is not equal to itself. So only use this optimization on format known to + // For formats supported by the struct module a memoryview is equal to + // itself: there is no need to compare individual values. + // This is not true for float values since they can be NaN, and NaN + // is not equal to itself. So only use this optimization on format known to // not use floats. if (v == w) { int can_compare_ptr; const char *format = vv->format; if (format != NULL) { - // Exclude formats "d" (double), "f" (float), "e" (16-bit float) - // and "P" (void*) + // Include only formats known by struct, exclude formats "d" (double), + // "f" (float), "e" (16-bit float) and "P" (void*) can_compare_ptr = (strchr("bBchHiIlLnNqQ?", format[0]) != NULL && format[1] == 0); } From 2316faba72c2d5d85408276fc6c5275a56d7685b Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Sat, 21 Feb 2026 13:27:40 +0100 Subject: [PATCH 5/8] Address review * Optimize also "P" format * Test also "m != m" * Handle native formats such as "@B" --- Lib/test/test_memoryview.py | 3 +++ Objects/memoryobject.c | 7 +++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_memoryview.py b/Lib/test/test_memoryview.py index 5aa2b0ecd6767d..0d40820d2dc88b 100644 --- a/Lib/test/test_memoryview.py +++ b/Lib/test/test_memoryview.py @@ -584,6 +584,7 @@ def test_compare_equal(self): a = array.array(int_format, [1, 2, 3]) m = memoryview(a) self.assertTrue(m == m) + self.assertFalse(m != m) for float_format in 'fd': with self.subTest(format=int_format): @@ -591,10 +592,12 @@ def test_compare_equal(self): m = memoryview(a) # nan is not equal to nan self.assertFalse(m == m) + self.assertTrue(m != m) a = array.array(float_format, [1.0, 2.0, 3.0]) m = memoryview(a) self.assertTrue(m == m) + self.assertFalse(m != m) class BytesMemorySliceTest(unittest.TestCase, diff --git a/Objects/memoryobject.c b/Objects/memoryobject.c index 76ac3f4d059c60..10fa98635af07b 100644 --- a/Objects/memoryobject.c +++ b/Objects/memoryobject.c @@ -3131,9 +3131,12 @@ memory_richcompare(PyObject *v, PyObject *w, int op) int can_compare_ptr; const char *format = vv->format; if (format != NULL) { + if (*format == '@') { + format++; + } // Include only formats known by struct, exclude formats "d" (double), - // "f" (float), "e" (16-bit float) and "P" (void*) - can_compare_ptr = (strchr("bBchHiIlLnNqQ?", format[0]) != NULL + // "f" (float), "e" (16-bit float) + can_compare_ptr = (strchr("bBchHiIlLnNPqQ?", format[0]) != NULL && format[1] == 0); } else { From 800e85f7446d6bc1160f3ef0f949baf662b7ffe1 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Sat, 21 Feb 2026 16:24:05 +0100 Subject: [PATCH 6/8] Test more formats: @b, @B, P and ? --- Lib/test/test_memoryview.py | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_memoryview.py b/Lib/test/test_memoryview.py index 0d40820d2dc88b..cc906659e67077 100644 --- a/Lib/test/test_memoryview.py +++ b/Lib/test/test_memoryview.py @@ -579,6 +579,8 @@ def test_compare_equal(self): # A memoryview is equal to itself: there is no need to compare # individual values. This is not true for float values since they can # be NaN, and NaN is not equal to itself. + + # Test integer formats for int_format in 'bBhHiIlLqQ': with self.subTest(format=int_format): a = array.array(int_format, [1, 2, 3]) @@ -586,8 +588,31 @@ def test_compare_equal(self): self.assertTrue(m == m) self.assertFalse(m != m) + if int_format in 'bB': + m2 = m.cast('@' + m.format) + self.assertTrue(m2 == m2) + self.assertFalse(m2 != m2) + + # Test '?' format + m = memoryview(b'\0\1\2').cast('?') + self.assertTrue(m == m) + self.assertFalse(m != m) + + # Test 'P' format + if struct.calcsize('L') == struct.calcsize('P'): + int_format = 'L' + elif struct.calcsize('Q') == struct.calcsize('P'): + int_format = 'Q' + else: + raise ValueError('unable to get void* format in struct') + a = array.array(int_format, [1, 2, 3]) + m = memoryview(a.tobytes()).cast('P') + self.assertTrue(m == m) + self.assertFalse(m != m) + + # Test float formats for float_format in 'fd': - with self.subTest(format=int_format): + with self.subTest(format=float_format): a = array.array(float_format, [1.0, 2.0, float('nan')]) m = memoryview(a) # nan is not equal to nan From a754be418e546d6619b61cde22fd5375ca51f1dd Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Sat, 21 Feb 2026 19:50:55 +0100 Subject: [PATCH 7/8] Fix for empty format string --- Objects/memoryobject.c | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Objects/memoryobject.c b/Objects/memoryobject.c index 10fa98635af07b..cbff6daafdceaf 100644 --- a/Objects/memoryobject.c +++ b/Objects/memoryobject.c @@ -3134,9 +3134,10 @@ memory_richcompare(PyObject *v, PyObject *w, int op) if (*format == '@') { format++; } - // Include only formats known by struct, exclude formats "d" (double), - // "f" (float), "e" (16-bit float) - can_compare_ptr = (strchr("bBchHiIlLnNPqQ?", format[0]) != NULL + // Include only formats known by struct, exclude formats + // "d" (double), "f" (float) and "e" (16-bit float). + can_compare_ptr = (format[0] != 0 + && strchr("bBchHiIlLnNPqQ?", format[0]) != NULL && format[1] == 0); } else { From 1d72ed29a4114a5638eaa83104f139731c522642 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Sat, 21 Feb 2026 20:01:27 +0100 Subject: [PATCH 8/8] Add tests with optimization disabled --- Lib/test/test_memoryview.py | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/Lib/test/test_memoryview.py b/Lib/test/test_memoryview.py index cc906659e67077..3cabb2724a8903 100644 --- a/Lib/test/test_memoryview.py +++ b/Lib/test/test_memoryview.py @@ -580,23 +580,30 @@ def test_compare_equal(self): # individual values. This is not true for float values since they can # be NaN, and NaN is not equal to itself. + def check_equal(view, is_equal): + self.assertEqual(view == view, is_equal) + self.assertEqual(view != view, not is_equal) + + # Comparison with a different memoryview doesn't use + # the optimization and should give the same result. + view2 = memoryview(view) + self.assertEqual(view2 == view, is_equal) + self.assertEqual(view2 != view2, not is_equal) + # Test integer formats for int_format in 'bBhHiIlLqQ': with self.subTest(format=int_format): a = array.array(int_format, [1, 2, 3]) m = memoryview(a) - self.assertTrue(m == m) - self.assertFalse(m != m) + check_equal(m, True) if int_format in 'bB': m2 = m.cast('@' + m.format) - self.assertTrue(m2 == m2) - self.assertFalse(m2 != m2) + check_equal(m2, True) # Test '?' format m = memoryview(b'\0\1\2').cast('?') - self.assertTrue(m == m) - self.assertFalse(m != m) + check_equal(m, True) # Test 'P' format if struct.calcsize('L') == struct.calcsize('P'): @@ -607,8 +614,7 @@ def test_compare_equal(self): raise ValueError('unable to get void* format in struct') a = array.array(int_format, [1, 2, 3]) m = memoryview(a.tobytes()).cast('P') - self.assertTrue(m == m) - self.assertFalse(m != m) + check_equal(m, True) # Test float formats for float_format in 'fd': @@ -616,13 +622,11 @@ def test_compare_equal(self): a = array.array(float_format, [1.0, 2.0, float('nan')]) m = memoryview(a) # nan is not equal to nan - self.assertFalse(m == m) - self.assertTrue(m != m) + check_equal(m, False) a = array.array(float_format, [1.0, 2.0, 3.0]) m = memoryview(a) - self.assertTrue(m == m) - self.assertFalse(m != m) + check_equal(m, True) class BytesMemorySliceTest(unittest.TestCase,