From 4dbee9ad373de2948503d8e053c337f792286ef4 Mon Sep 17 00:00:00 2001 From: Chad Netzer Date: Sat, 20 Mar 2021 19:37:57 -0700 Subject: [PATCH 01/11] Add testcase for bpo-43574 list overallocaton --- Lib/test/test_list.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Lib/test/test_list.py b/Lib/test/test_list.py index 3c8d82958fd7c8..cf70e665f9c888 100644 --- a/Lib/test/test_list.py +++ b/Lib/test/test_list.py @@ -196,6 +196,15 @@ def test_preallocation(self): self.assertEqual(iter_size, sys.getsizeof(list([0] * 10))) self.assertEqual(iter_size, sys.getsizeof(list(range(10)))) + @cpython_only + def test_overallocation(self): + iterable = [1,2] + self.assertEqual(sys.getsizeof(iterable), sys.getsizeof(list(iterable))) + + # bpo-43574: Don't overallocate for list literals + iterable = [1,2,3] + self.assertEqual(sys.getsizeof(iterable), sys.getsizeof(list(iterable))) + def test_count_index_remove_crashes(self): # bpo-38610: The count(), index(), and remove() methods were not # holding strong references to list elements while calling From 5b31470b03dcabd9f687022ed724348ccc01fb7b Mon Sep 17 00:00:00 2001 From: Chad Netzer Date: Sat, 20 Mar 2021 19:01:01 -0700 Subject: [PATCH 02/11] Don't overallocate when extending an empty list This restores the behavior of Python versions before v3.9, where a list initialized from a list literal allocates memory for exactly as many elements as it contains (until a list append or other operation that changes the list size). The interpreter was changed in v3.9 to use the LIST_EXTEND bytecode to initialize a new list from a literal, which caused it to overallocate. By not performing this over-allocation on an empty list, we restored the original behavior (though now explicitly creating an empty list and extending it once will also not result in an over-allocation). --- Objects/listobject.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Objects/listobject.c b/Objects/listobject.c index e7987a6d352bfa..e7998f1dea3502 100644 --- a/Objects/listobject.c +++ b/Objects/listobject.c @@ -75,8 +75,9 @@ list_resize(PyListObject *self, Py_ssize_t newsize) if (newsize - Py_SIZE(self) > (Py_ssize_t)(new_allocated - newsize)) new_allocated = ((size_t)newsize + 3) & ~(size_t)3; - if (newsize == 0) - new_allocated = 0; + /* Don't overallocate for lists that start empty or are set to empty. */ + if (newsize == 0 || Py_SIZE(self) == 0) + new_allocated = newsize; num_allocated_bytes = new_allocated * sizeof(PyObject *); items = (PyObject **)PyMem_Realloc(self->ob_item, num_allocated_bytes); if (items == NULL) { From 172f2c0a4014805890ec7cbd16ea3625a569e249 Mon Sep 17 00:00:00 2001 From: Chad Netzer Date: Sat, 20 Mar 2021 19:56:05 -0700 Subject: [PATCH 03/11] Skip overallocation calc when not overallocating --- Objects/listobject.c | 40 +++++++++++++++++++++------------------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/Objects/listobject.c b/Objects/listobject.c index e7998f1dea3502..5a4da95452bcf8 100644 --- a/Objects/listobject.c +++ b/Objects/listobject.c @@ -58,26 +58,28 @@ list_resize(PyListObject *self, Py_ssize_t newsize) return 0; } - /* This over-allocates proportional to the list size, making room - * for additional growth. The over-allocation is mild, but is - * enough to give linear-time amortized behavior over a long - * sequence of appends() in the presence of a poorly-performing - * system realloc(). - * Add padding to make the allocated size multiple of 4. - * The growth pattern is: 0, 4, 8, 16, 24, 32, 40, 52, 64, 76, ... - * Note: new_allocated won't overflow because the largest possible value - * is PY_SSIZE_T_MAX * (9 / 8) + 6 which always fits in a size_t. - */ - new_allocated = ((size_t)newsize + (newsize >> 3) + 6) & ~(size_t)3; - /* Do not overallocate if the new size is closer to overallocated size - * than to the old size. - */ - if (newsize - Py_SIZE(self) > (Py_ssize_t)(new_allocated - newsize)) - new_allocated = ((size_t)newsize + 3) & ~(size_t)3; - - /* Don't overallocate for lists that start empty or are set to empty. */ - if (newsize == 0 || Py_SIZE(self) == 0) + if (newsize == 0 || Py_SIZE(self) == 0) { + /* Don't overallocate for lists that start empty or are set to empty. */ new_allocated = newsize; + } else { + /* This over-allocates proportional to the list size, making room + * for additional growth. The over-allocation is mild, but is + * enough to give linear-time amortized behavior over a long + * sequence of appends() in the presence of a poorly-performing + * system realloc(). + * Add padding to make the allocated size multiple of 4. + * The growth pattern is: 0, 4, 8, 16, 24, 32, 40, 52, 64, 76, ... + * Note: new_allocated won't overflow because the largest possible value + * is PY_SSIZE_T_MAX * (9 / 8) + 6 which always fits in a size_t. + */ + new_allocated = ((size_t)newsize + (newsize >> 3) + 6) & ~(size_t)3; + /* Do not overallocate if the new size is closer to overallocated size + * than to the old size. + */ + if (newsize - Py_SIZE(self) > (Py_ssize_t)(new_allocated - newsize)) + new_allocated = ((size_t)newsize + 3) & ~(size_t)3; + } + num_allocated_bytes = new_allocated * sizeof(PyObject *); items = (PyObject **)PyMem_Realloc(self->ob_item, num_allocated_bytes); if (items == NULL) { From d76ae2a9adb9892514e804a47b77cd5738ff5195 Mon Sep 17 00:00:00 2001 From: Chad Netzer Date: Sun, 21 Mar 2021 19:10:42 -0700 Subject: [PATCH 04/11] Fix list_extend() init list changes to PEP-7 form --- Objects/listobject.c | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Objects/listobject.c b/Objects/listobject.c index 5a4da95452bcf8..729e79ef5664bf 100644 --- a/Objects/listobject.c +++ b/Objects/listobject.c @@ -61,7 +61,8 @@ list_resize(PyListObject *self, Py_ssize_t newsize) if (newsize == 0 || Py_SIZE(self) == 0) { /* Don't overallocate for lists that start empty or are set to empty. */ new_allocated = newsize; - } else { + } + else { /* This over-allocates proportional to the list size, making room * for additional growth. The over-allocation is mild, but is * enough to give linear-time amortized behavior over a long @@ -76,8 +77,9 @@ list_resize(PyListObject *self, Py_ssize_t newsize) /* Do not overallocate if the new size is closer to overallocated size * than to the old size. */ - if (newsize - Py_SIZE(self) > (Py_ssize_t)(new_allocated - newsize)) + if (newsize - Py_SIZE(self) > (Py_ssize_t)(new_allocated - newsize)) { new_allocated = ((size_t)newsize + 3) & ~(size_t)3; + } } num_allocated_bytes = new_allocated * sizeof(PyObject *); From 7aad246f17772e19d115a1dcd8f670dfa92fb22d Mon Sep 17 00:00:00 2001 From: Chad Netzer Date: Sun, 21 Mar 2021 19:11:44 -0700 Subject: [PATCH 05/11] bpo-43574: Add NEWS --- .../Core and Builtins/2021-03-21-18-51-30.bpo-43574.mteI-I.rst | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 Misc/NEWS.d/next/Core and Builtins/2021-03-21-18-51-30.bpo-43574.mteI-I.rst diff --git a/Misc/NEWS.d/next/Core and Builtins/2021-03-21-18-51-30.bpo-43574.mteI-I.rst b/Misc/NEWS.d/next/Core and Builtins/2021-03-21-18-51-30.bpo-43574.mteI-I.rst new file mode 100644 index 00000000000000..98dffe4669745c --- /dev/null +++ b/Misc/NEWS.d/next/Core and Builtins/2021-03-21-18-51-30.bpo-43574.mteI-I.rst @@ -0,0 +1,2 @@ +Restores previous list memory behavior where lists initialized from literals +aren't over-allocated. From 7c20c6cc15873cbbc7f94c06727ce1f85c27b855 Mon Sep 17 00:00:00 2001 From: Chad Netzer Date: Sun, 21 Mar 2021 18:39:51 -0700 Subject: [PATCH 06/11] Add more list overallocation regression tests Ensure that lists initialized from both list-literals and iterables of known length are initialized without over-allocation, by comparing them with allocation of lists initialized from an unknown size where over-allocation is expected. --- Lib/test/test_list.py | 46 +++++++++++++++++++++++++++++++++++++------ 1 file changed, 40 insertions(+), 6 deletions(-) diff --git a/Lib/test/test_list.py b/Lib/test/test_list.py index cf70e665f9c888..0fdcf4d72c6433 100644 --- a/Lib/test/test_list.py +++ b/Lib/test/test_list.py @@ -198,12 +198,46 @@ def test_preallocation(self): @cpython_only def test_overallocation(self): - iterable = [1,2] - self.assertEqual(sys.getsizeof(iterable), sys.getsizeof(list(iterable))) - - # bpo-43574: Don't overallocate for list literals - iterable = [1,2,3] - self.assertEqual(sys.getsizeof(iterable), sys.getsizeof(list(iterable))) + # bpo-33234: Don't overallocate when initialized from known lengths + # bpo-38373: Allows list over-allocation to be zero for some lengths + # bpo-43574: Don't overallocate for list-literals + sizeof = sys.getsizeof + + # First handle empty list and empty list-literal cases. Should have no + # overallocation, including init from iterable of unknown length. + self.assertEqual(sizeof([]), sizeof(list())) + self.assertEqual(sizeof([]), sizeof(list(tuple()))) + self.assertEqual(sizeof([]), sizeof(list(x for x in []))) + + # Must use actual list-literals to test the overallocation behavior of + # compiled list-literals as well as those initialized from them. + test_literals = [ + [1], + [1,2], + [1,2,3], # Literals of length > 2 are special-cased in compile + [1,2,3,4], + [1,2,3,4,5,6,7], + [1,2,3,4,5,6,7,8], # bpo-38373: Length 8 init won't over-alloc + [1,2,3,4,5,6,7,8,9], + ] + + overalloc_amts = [] + for literal in test_literals: + # Ensure that both list literals, and lists made from an iterable + # of known size use the same amount of allocation. + self.assertEqual(sizeof(literal), sizeof(list(literal))) + self.assertEqual(sizeof(literal), sizeof(list(tuple(literal)))) + + # By contrast, confirm that non-empty lists initialized from an + # iterable where the length is unknown at the time of + # initialization, can be overallocated. + iterated_list = list(x for x in literal) + overalloc_amts.append(sizeof(iterated_list) - sizeof(literal)) + self.assertGreaterEqual(sizeof(iterated_list), sizeof(literal)) + + # bpo-38373: initialized or grown lists are not always over-allocated. + # Confirm that over-allocation occurs at least some of the time. + self.assertEqual(True, any(x>0 for x in overalloc_amts)) def test_count_index_remove_crashes(self): # bpo-38610: The count(), index(), and remove() methods were not From 7be3ae871e2e435fbe67c372c3dfaeca8cfa0927 Mon Sep 17 00:00:00 2001 From: Chad Netzer Date: Mon, 22 Mar 2021 09:38:31 -0700 Subject: [PATCH 07/11] Put common-case first in logical-or list_resize() Micro-optimization --- Objects/listobject.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Objects/listobject.c b/Objects/listobject.c index 729e79ef5664bf..1f195118c10f9e 100644 --- a/Objects/listobject.c +++ b/Objects/listobject.c @@ -58,7 +58,7 @@ list_resize(PyListObject *self, Py_ssize_t newsize) return 0; } - if (newsize == 0 || Py_SIZE(self) == 0) { + if (Py_SIZE(self) == 0 || newsize == 0) { /* Don't overallocate for lists that start empty or are set to empty. */ new_allocated = newsize; } From 7436223a71b2ed46fff050d956d5effa5acbd549 Mon Sep 17 00:00:00 2001 From: Chad Netzer Date: Tue, 23 Mar 2021 12:38:30 -0700 Subject: [PATCH 08/11] Adjust NEWS entry language based on PR feedback --- .../2021-03-21-18-51-30.bpo-43574.mteI-I.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Misc/NEWS.d/next/Core and Builtins/2021-03-21-18-51-30.bpo-43574.mteI-I.rst b/Misc/NEWS.d/next/Core and Builtins/2021-03-21-18-51-30.bpo-43574.mteI-I.rst index 98dffe4669745c..818ea4465f4731 100644 --- a/Misc/NEWS.d/next/Core and Builtins/2021-03-21-18-51-30.bpo-43574.mteI-I.rst +++ b/Misc/NEWS.d/next/Core and Builtins/2021-03-21-18-51-30.bpo-43574.mteI-I.rst @@ -1,2 +1,3 @@ -Restores previous list memory behavior where lists initialized from literals -aren't over-allocated. +``list`` objects don't overallocate when starting empty and then extended, or +when set to be empty. This effectively restores previous ``list`` memory +behavior for lists initialized from literals. From 657d51fcb25d7653fff91c044fde3452b2bce056 Mon Sep 17 00:00:00 2001 From: Chad Netzer Date: Wed, 24 Mar 2021 00:23:45 -0700 Subject: [PATCH 09/11] Allow empty list append/insert to overallocate The list_resize() helper is used by single-item list appends and inserts, which normally would overallocate (to a capacity of 4) on the first added element, and then again to 8 when the capacity is filled. But if this over-allocation is postponed on the first added element, the current expansion formula would then over-allocate to a capacity of 8 on the second append/insertion. List-literals of length 1 and 2 are not created with the list_extend() function (which then calls list_resize()), but instead built directly as capacity 1 or 2 lists with the BUILD_LIST opcode. By excluding the special case for list_resize() of empty lists when newsize is greater than 1, its allows list append/insert to continue to over-allocate without skipping capacity 4. --- Objects/listobject.c | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Objects/listobject.c b/Objects/listobject.c index 1f195118c10f9e..05b0de144c71a9 100644 --- a/Objects/listobject.c +++ b/Objects/listobject.c @@ -58,8 +58,11 @@ list_resize(PyListObject *self, Py_ssize_t newsize) return 0; } - if (Py_SIZE(self) == 0 || newsize == 0) { - /* Don't overallocate for lists that start empty or are set to empty. */ + if (newsize == 0 || (Py_SIZE(self) == 0 && newsize > 1)) { + /* Don't overallocate empty lists that are extended by more than 1 + * element. This helps ensure that list-literals aren't + * over-allocated, but still allows it for empty-list append/insert. + */ new_allocated = newsize; } else { From 4336cc6060188b1af9976dd785696b53b865031e Mon Sep 17 00:00:00 2001 From: Chad Netzer Date: Thu, 25 Mar 2021 22:30:58 -0700 Subject: [PATCH 10/11] Update test_overallocation of empty list append() --- Lib/test/test_list.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_list.py b/Lib/test/test_list.py index 0fdcf4d72c6433..d04e247d0017b3 100644 --- a/Lib/test/test_list.py +++ b/Lib/test/test_list.py @@ -224,7 +224,8 @@ def test_overallocation(self): overalloc_amts = [] for literal in test_literals: # Ensure that both list literals, and lists made from an iterable - # of known size use the same amount of allocation. + # of known size use the same amount of allocation. It will be + # verified later that no over-allocation occurs for list literals. self.assertEqual(sizeof(literal), sizeof(list(literal))) self.assertEqual(sizeof(literal), sizeof(list(tuple(literal)))) @@ -239,6 +240,15 @@ def test_overallocation(self): # Confirm that over-allocation occurs at least some of the time. self.assertEqual(True, any(x>0 for x in overalloc_amts)) + # Empty lists should overallocate on initial append/insert (unlike + # list-literals) + l1 = [] + l1.append(1) + self.assertGreater(sizeof(l1), sizeof([1])) + l2 = [] + l2.insert(0, 1) + self.assertGreater(sizeof(l2), sizeof([1])) + def test_count_index_remove_crashes(self): # bpo-38610: The count(), index(), and remove() methods were not # holding strong references to list elements while calling From 19d43741f7cf16a1bf6e9a9094e469836a3d5c4d Mon Sep 17 00:00:00 2001 From: Chad Netzer Date: Sun, 4 Apr 2021 17:06:38 -0700 Subject: [PATCH 11/11] Add direct test of list-literal non-overallocation Confirm that list-literals aren't over-allocated by directly checking their used memory, which is the combination of the size of an empty list and the number of pointers needed for a list of exactly that length. --- Lib/test/test_list.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_list.py b/Lib/test/test_list.py index d04e247d0017b3..f96f5b03fa8187 100644 --- a/Lib/test/test_list.py +++ b/Lib/test/test_list.py @@ -2,6 +2,7 @@ from test import list_tests from test.support import cpython_only import pickle +import struct import unittest class ListTest(list_tests.CommonTest): @@ -223,9 +224,13 @@ def test_overallocation(self): overalloc_amts = [] for literal in test_literals: + # Direct check that list-literals do not over-allocate, by + # calculating the total size of used pointers. + total_ptr_size = len(literal) * struct.calcsize('P') + self.assertEqual(sizeof(literal), sizeof([]) + total_ptr_size) + # Ensure that both list literals, and lists made from an iterable - # of known size use the same amount of allocation. It will be - # verified later that no over-allocation occurs for list literals. + # of known size, use the same amount of allocation. self.assertEqual(sizeof(literal), sizeof(list(literal))) self.assertEqual(sizeof(literal), sizeof(list(tuple(literal))))