Ответ 1
Я думаю, что вы видите шаблоны перераспределения, это выборка из источника:
/* 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().
* The growth pattern is: 0, 4, 8, 16, 25, 35, 46, 58, 72, 88, ...
*/
new_allocated = (newsize >> 3) + (newsize < 9 ? 3 : 6);
Распечатав размеры списков с длинами 0-88, вы можете увидеть совпадения шаблонов:
# create comprehensions for sizes 0-88
comprehensions = [sys.getsizeof([1 for _ in range(l)]) for l in range(90)]
# only take those that resulted in growth compared to previous length
steps = zip(comprehensions, comprehensions[1:])
growths = [x for x in list(enumerate(steps)) if x[1][0] != x[1][1]]
# print the results:
for growth in growths:
print(growth)
Результаты (формат (list length, (old total size, new total size))
):
(0, (64, 96))
(4, (96, 128))
(8, (128, 192))
(16, (192, 264))
(25, (264, 344))
(35, (344, 432))
(46, (432, 528))
(58, (528, 640))
(72, (640, 768))
(88, (768, 912))
Перераспределение выполняется по соображениям производительности, позволяя увеличивать списки без выделения большего объема памяти при каждом увеличении (улучшенная амортизированная производительность).
Вероятная причина различий с использованием понимания списка состоит в том, что понимание списка не может детерминистически рассчитать размер сгенерированного списка, но list()
может. Это означает, что понимания будут постоянно увеличивать список, поскольку он заполняет его, используя перераспределение, пока, наконец, не заполнит его.
Возможно, что это не приведет к увеличению буфера перераспределения с неиспользованными выделенными узлами, как только это будет сделано (фактически, в большинстве случаев это будет невозможно, что приведет к поражению цели перераспределения).
list()
, однако, может добавить некоторый буфер независимо от размера списка, так как он заранее знает окончательный размер списка.
Еще одним подтверждением, также из источника, является то, что мы видим, что список вызывает LIST_APPEND
, который указывает на использование list.resize
, что, в свою очередь, указывает на использование буфера предварительного выделения, не зная, сколько его будет заполнено. Это соответствует поведению, которое вы видите.
В заключение, list()
будет предварительно выделять больше узлов как функцию размера списка
>>> sys.getsizeof(list([1,2,3]))
60
>>> sys.getsizeof(list([1,2,3,4]))
64
Понимание списка не знает размера списка, поэтому оно использует операции добавления по мере роста, истощая буфер предварительного выделения:
# one item before filling pre-allocation buffer completely
>>> sys.getsizeof([i for i in [1,2,3]])
52
# fills pre-allocation buffer completely
# note that size did not change, we still have buffered unused nodes
>>> sys.getsizeof([i for i in [1,2,3,4]])
52
# grows pre-allocation buffer
>>> sys.getsizeof([i for i in [1,2,3,4,5]])
68