Skip to content

Commit 748ac24

Browse files
committed
Finish() shrinks the list
Add test_empty() and test_finish_shrink() tests. Add tests on MemoryError. Move LIST_SMALL_ALLOCATED to pycore_list.h as _Py_LIST_SMALL_ALLOCATED.
1 parent da86200 commit 748ac24

4 files changed

Lines changed: 140 additions & 10 deletions

File tree

‎Include/internal/pycore_list.h‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ extern "C" {
1212
#include "pycore_stackref.h"
1313
#endif
1414

15+
#define _Py_LIST_SMALL_ALLOCATED 32
16+
1517
PyAPI_FUNC(PyObject*) _PyList_Extend(PyListObject *, PyObject *);
1618
PyAPI_FUNC(PyObject) *_PyList_SliceSubscript(PyObject*, PyObject*);
1719
PyAPI_FUNC(PyObject *) _PyList_BinarySlice(PyObject *, PyObject *, PyObject *);

‎Lib/test/test_capi/test_list.py‎

Lines changed: 95 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import threading
22
import unittest
33
from collections import UserList
4+
from test import support
45
from test.support import import_helper
56
from test.support import threading_helper
67

@@ -11,6 +12,7 @@
1112
NULL = None
1213
PY_SSIZE_T_MIN = _testcapi.PY_SSIZE_T_MIN
1314
PY_SSIZE_T_MAX = _testcapi.PY_SSIZE_T_MAX
15+
LIST_SMALL_ALLOCATED = _testcapi.LIST_SMALL_ALLOCATED
1416

1517
class ListSubclass(list):
1618
pass
@@ -360,7 +362,7 @@ def test_uninitialized_list_repr(self):
360362

361363

362364
class ListWriterTests(unittest.TestCase):
363-
def create_writer(self, size=0):
365+
def create_writer(self, size):
364366
# Test PyListWriter_Create()
365367
return _testcapi.PyListWriter(size)
366368

@@ -385,6 +387,16 @@ def check_append(self, method):
385387
append(item)
386388
self.list_equal(writer.finish(), items)
387389

390+
# test MemoryError
391+
writer = self.create_writer(0)
392+
append = getattr(writer, method)
393+
with self.assertRaises(MemoryError):
394+
with support.inject_memory_error_cm():
395+
append("alloc failed")
396+
for item in items:
397+
append(item)
398+
self.list_equal(writer.finish(), items)
399+
388400
def test_append(self):
389401
# Test PyListWriter_Append()
390402
self.check_append('append')
@@ -402,6 +414,16 @@ def check_extend(self, method):
402414
extend(items)
403415
self.list_equal(writer.finish(), items)
404416

417+
# test MemoryError
418+
writer = self.create_writer(0)
419+
extend = getattr(writer, method)
420+
items_error = ("alloc failed",)
421+
with self.assertRaises(MemoryError):
422+
with support.inject_memory_error_cm():
423+
extend(items_error)
424+
extend(items)
425+
self.list_equal(writer.finish(), items)
426+
405427
def test_extend(self):
406428
# Test PyListWriter_Extend()
407429
self.check_extend('extend')
@@ -414,27 +436,94 @@ def test_extend_array_steal(self):
414436
# Test PyListWriter_ExtendArraySteal()
415437
self.check_extend('extend_array_steal')
416438

439+
def test_empty(self):
440+
list_get_allocated = _testcapi._list_get_allocated
441+
empty_tuple = ()
442+
443+
for size in (0, LIST_SMALL_ALLOCATED, 1024):
444+
with self.subTest(size=size):
445+
writer = self.create_writer(size)
446+
writer.extend(empty_tuple)
447+
writer.extend_array(empty_tuple)
448+
writer.extend_array_steal(empty_tuple)
449+
result = writer.finish()
450+
self.list_equal(result, [])
451+
452+
if size == 0 or size > LIST_SMALL_ALLOCATED:
453+
expected = 0
454+
else:
455+
expected = size
456+
self.assertEqual(list_get_allocated(result), expected)
457+
458+
def test_finish_shrink(self):
459+
# PyListWriter_Finish() shrinks the list if its allocated size
460+
# is way too big
461+
list_get_allocated = _testcapi._list_get_allocated
462+
463+
def compute_allocated(size):
464+
return (size + (size >> 3) + 6) & ~3
465+
466+
def check_allocated(items, alloc, expected):
467+
writer = self.create_writer(alloc)
468+
# Don't use PyListWriter_Extend() since it can shrink the list
469+
for item in items:
470+
writer.append(item)
471+
result = writer.finish()
472+
self.list_equal(result, list(items))
473+
self.assertEqual(list_get_allocated(result), expected)
474+
475+
# small list
476+
items = [1, 2, 3]
477+
check_allocated(items,
478+
1024,
479+
compute_allocated(len(items)))
480+
481+
alloc = LIST_SMALL_ALLOCATED
482+
check_allocated(items, alloc, alloc)
483+
484+
# larger list
485+
items = tuple(range(100))
486+
check_allocated(items,
487+
(len(items) + 1) * 2,
488+
compute_allocated(len(items)))
489+
490+
alloc = len(items) * 2
491+
check_allocated(items, alloc, alloc)
492+
493+
# do not shrink on MemoryError
494+
alloc = 1024
495+
writer = self.create_writer(alloc)
496+
writer.append(1)
497+
with support.inject_memory_error_cm():
498+
# list_resize() never fails when shrinking
499+
result = writer.finish()
500+
self.list_equal(result, [1])
501+
self.assertEqual(list_get_allocated(result), alloc)
502+
417503
def test_discard(self):
418504
# Test PyListWriter_Discard(): must not leak memory
419-
writer = self.create_writer()
505+
writer = self.create_writer(0)
420506
writer.extend([2, 5, 7])
421507
writer.discard()
422508

423509
@threading_helper.requires_working_threading()
424510
def test_thread(self):
425511
# PyListWriter can be used by multiple threads: it's up to the caller
426512
# to implement a lock to prevent concurrent accesses.
427-
writer = self.create_writer()
513+
writer = self.create_writer(0)
428514

429515
def thread_func(writer):
430-
writer.append('Hello')
431-
writer.extend(['World!'])
516+
writer.append('a')
517+
writer.append_steal('b')
518+
writer.extend(('c',))
519+
writer.extend_array(('d',))
520+
writer.extend_array_steal(('e',))
432521

433522
thread = threading.Thread(target=thread_func, args=(writer,))
434523
thread.start()
435524
threading_helper.join_thread(thread)
436525

437-
self.list_equal(writer.finish(), ['Hello', 'World!'])
526+
self.list_equal(writer.finish(), list('abcde'))
438527

439528

440529
if __name__ == "__main__":

‎Modules/_testcapi/list.c‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
1+
// Use pycore_list.h
2+
#define PYTESTCAPI_NEED_INTERNAL_API
3+
14
#include "parts.h"
25
#include "util.h"
36

7+
#include "pycore_list.h" // _Py_LIST_SMALL_ALLOCATED
8+
49

510
static PyObject *
611
list_get_size(PyObject *Py_UNUSED(module), PyObject *obj)
@@ -103,6 +108,15 @@ test_list_api(PyObject *self, PyObject *Py_UNUSED(ignored))
103108
}
104109

105110

111+
static PyObject *
112+
_list_get_allocated(PyObject *Py_UNUSED(module), PyObject *obj)
113+
{
114+
NULLABLE(obj);
115+
PyListObject *list = (PyListObject*)obj;
116+
return PyLong_FromSsize_t(list->allocated);
117+
}
118+
119+
106120
typedef struct {
107121
PyObject_HEAD
108122
PyListWriter *writer;
@@ -338,6 +352,7 @@ static PyMethodDef test_methods[] = {
338352
{"list_clear", list_clear, METH_O},
339353
{"list_extend", list_extend, METH_VARARGS},
340354
{"test_list_api", test_list_api, METH_NOARGS},
355+
{"_list_get_allocated", _list_get_allocated, METH_O},
341356
{NULL},
342357
};
343358

@@ -358,5 +373,9 @@ _PyTestCapi_Init_List(PyObject *m)
358373
}
359374
Py_DECREF(writer_type);
360375

376+
if (PyModule_AddIntConstant(m, "LIST_SMALL_ALLOCATED", _Py_LIST_SMALL_ALLOCATED) < 0) {
377+
return -1;
378+
}
379+
361380
return 0;
362381
}

‎Objects/listobject.c‎

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ ensure_shared_on_resize(PyListObject *self)
9191
#endif
9292
}
9393

94-
#define LIST_SMALL_ALLOCATED 32
94+
#define LIST_SMALL_ALLOCATED _Py_LIST_SMALL_ALLOCATED
9595

9696
static int py_list_resize(PyListObject *self, Py_ssize_t newsize);
9797

@@ -4419,7 +4419,10 @@ static int
44194419
listwriter_check_consistency(PyListWriter *writer)
44204420
{
44214421
assert(writer != NULL);
4422-
PyObject *list = _PyObject_CAST(writer);
4422+
assert(PyList_CheckExact(_PyObject_CAST(writer)));
4423+
PyListObject *list = (PyListObject*)writer;
4424+
assert(Py_SIZE(list) >= 0);
4425+
assert(Py_SIZE(list) <= list->allocated);
44234426
// Don't test _PyObject_IsUniquelyReferenced() since a list writer can
44244427
// be shared between multiple threads with external locking.
44254428
assert(Py_REFCNT(list) == 1);
@@ -4479,6 +4482,22 @@ PyListWriter_Finish(PyListWriter *writer)
44794482
assert(listwriter_check_consistency(writer));
44804483
PyListObject *list = _PyList_CAST(writer);
44814484

4485+
// Shrink the list if the allocated size is greater than the double size.
4486+
// For example, if PyListWriter_Create() argument overestimated the final
4487+
// size.
4488+
if (Py_SIZE(list) < (list->allocated >> 1)
4489+
&& list->allocated > LIST_SMALL_ALLOCATED)
4490+
{
4491+
if (list_resize(list, Py_SIZE(list)) < 0) {
4492+
// This case cannot happen: list_resize() never fails when
4493+
// shrinking
4494+
PyListWriter_Discard(writer);
4495+
return NULL;
4496+
}
4497+
// Note: cannot test that list->allocated changed in debug mode, since
4498+
// it's left unchanged on memory allocation failure.
4499+
}
4500+
44824501
#ifdef Py_DEBUG
44834502
// Make sure that all items have been initialized
44844503
Py_ssize_t size = Py_SIZE(list);
@@ -4534,14 +4553,15 @@ PyListWriter_Append(PyListWriter *writer, PyObject *item)
45344553
}
45354554

45364555

4556+
// Similar to _list_extend() but don't lock the writer list.
45374557
// If you update this function, update also _list_extend().
45384558
int
45394559
PyListWriter_Extend(PyListWriter *writer, PyObject *iterable)
45404560
{
45414561
assert(listwriter_check_consistency(writer));
4542-
assert(_PyObject_CAST(writer) != iterable);
4543-
45444562
PyListObject *self = _PyList_CAST(writer);
4563+
assert(_PyObject_CAST(self) != iterable);
4564+
45454565
int res;
45464566
if (PyList_CheckExact(iterable)) {
45474567
Py_BEGIN_CRITICAL_SECTION(iterable);

0 commit comments

Comments
 (0)