From 22945429655ef17821a0f85611e467f87d9c404b Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Thu, 19 Feb 2026 12:46:53 +0100 Subject: [PATCH 01/11] 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 02/11] 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 03/11] 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 04/11] 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 05/11] 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 06/11] 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 07/11] 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 08/11] 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, From 134f0de290e1f9a878f9bd4eb7bcdfe8913de8d1 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Mon, 2 Mar 2026 22:46:25 +0100 Subject: [PATCH 09/11] Do not optimize "P" format Disable also the optimization if the format string is NULL. --- Lib/test/test_memoryview.py | 11 ----------- Objects/memoryobject.c | 5 +++-- 2 files changed, 3 insertions(+), 13 deletions(-) diff --git a/Lib/test/test_memoryview.py b/Lib/test/test_memoryview.py index 3cabb2724a8903..ccd90e2a65b3af 100644 --- a/Lib/test/test_memoryview.py +++ b/Lib/test/test_memoryview.py @@ -605,17 +605,6 @@ def check_equal(view, is_equal): m = memoryview(b'\0\1\2').cast('?') check_equal(m, True) - # 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') - check_equal(m, True) - # Test float formats for float_format in 'fd': with self.subTest(format=float_format): diff --git a/Objects/memoryobject.c b/Objects/memoryobject.c index cbff6daafdceaf..9ff1b2c9184164 100644 --- a/Objects/memoryobject.c +++ b/Objects/memoryobject.c @@ -3136,12 +3136,13 @@ memory_richcompare(PyObject *v, PyObject *w, int op) } // Include only formats known by struct, exclude formats // "d" (double), "f" (float) and "e" (16-bit float). + // Do not optimize "P" format. can_compare_ptr = (format[0] != 0 - && strchr("bBchHiIlLnNPqQ?", format[0]) != NULL + && strchr("bBchHiIlLnNqQ?", format[0]) != NULL && format[1] == 0); } else { - can_compare_ptr = 1; + can_compare_ptr = 0; } if (can_compare_ptr) { equal = 1; From 327252016beed3f61b7b4914856d678e16912d10 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Mon, 2 Mar 2026 22:53:15 +0100 Subject: [PATCH 10/11] Add tests on "c", "n" and "N" formats --- Lib/test/test_memoryview.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/Lib/test/test_memoryview.py b/Lib/test/test_memoryview.py index ccd90e2a65b3af..dc6985f5130a91 100644 --- a/Lib/test/test_memoryview.py +++ b/Lib/test/test_memoryview.py @@ -601,6 +601,24 @@ def check_equal(view, is_equal): m2 = m.cast('@' + m.format) check_equal(m2, True) + # Test 'c' format + a = array.array('B', [1, 2, 3]) + m = memoryview(a.tobytes()).cast('c') + check_equal(m, True) + + # Test 'n' and 'N' formats + if struct.calcsize('L') == struct.calcsize('N'): + int_format = 'L' + elif struct.calcsize('Q') == struct.calcsize('N'): + int_format = 'Q' + else: + raise ValueError('unable to get size_t format in struct') + a = array.array(int_format, [1, 2, 3]) + m = memoryview(a.tobytes()).cast('N') + check_equal(m, True) + m = memoryview(a.tobytes()).cast('n') + check_equal(m, True) + # Test '?' format m = memoryview(b'\0\1\2').cast('?') check_equal(m, True) From 83ad213b910c2629dfe2d99d25fe3d9773339ec1 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Tue, 3 Mar 2026 11:33:37 +0100 Subject: [PATCH 11/11] Address Serhiy's review * Skip "n" and "N" test if there is no struct format, instead of failing. * Remove can_compare_ptr variable. --- Lib/test/test_memoryview.py | 13 +++++++------ Objects/memoryobject.c | 20 ++++++++------------ 2 files changed, 15 insertions(+), 18 deletions(-) diff --git a/Lib/test/test_memoryview.py b/Lib/test/test_memoryview.py index dc6985f5130a91..0f7dc15b8c6f2c 100644 --- a/Lib/test/test_memoryview.py +++ b/Lib/test/test_memoryview.py @@ -612,12 +612,13 @@ def check_equal(view, is_equal): elif struct.calcsize('Q') == struct.calcsize('N'): int_format = 'Q' else: - raise ValueError('unable to get size_t format in struct') - a = array.array(int_format, [1, 2, 3]) - m = memoryview(a.tobytes()).cast('N') - check_equal(m, True) - m = memoryview(a.tobytes()).cast('n') - check_equal(m, True) + int_format = None + if int_format: + a = array.array(int_format, [1, 2, 3]) + m = memoryview(a.tobytes()).cast('N') + check_equal(m, True) + m = memoryview(a.tobytes()).cast('n') + check_equal(m, True) # Test '?' format m = memoryview(b'\0\1\2').cast('?') diff --git a/Objects/memoryobject.c b/Objects/memoryobject.c index 9ff1b2c9184164..0ad4f02d80bf50 100644 --- a/Objects/memoryobject.c +++ b/Objects/memoryobject.c @@ -3128,25 +3128,21 @@ 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_ptr; const char *format = vv->format; if (format != NULL) { if (*format == '@') { format++; } - // Include only formats known by struct, exclude formats + // Include only formats known by struct, exclude float formats // "d" (double), "f" (float) and "e" (16-bit float). // Do not optimize "P" format. - can_compare_ptr = (format[0] != 0 - && strchr("bBchHiIlLnNqQ?", format[0]) != NULL - && format[1] == 0); - } - else { - can_compare_ptr = 0; - } - if (can_compare_ptr) { - equal = 1; - goto result; + if (format[0] != 0 + && strchr("bBchHiIlLnNqQ?", format[0]) != NULL + && format[1] == 0) + { + equal = 1; + goto result; + } } }