List() использует немного больше памяти, чем понимание списка

Итак, я играл с объектами list и нашел немного странной вещи, что если list создается с помощью list(), он использует больше памяти, чем понимание списка? Я использую Python 3.5.2

In [1]: import sys
In [2]: a = list(range(100))
In [3]: sys.getsizeof(a)
Out[3]: 1008
In [4]: b = [i for i in range(100)]
In [5]: sys.getsizeof(b)
Out[5]: 912
In [6]: type(a) == type(b)
Out[6]: True
In [7]: a == b
Out[7]: True
In [8]: sys.getsizeof(list(b))
Out[8]: 1008

Из docs:

Списки могут быть сконструированы несколькими способами:

  • Использование пары квадратных скобок для обозначения пустого списка: []
  • Использование квадратных скобок, разделение элементов запятыми: [a], [a, b, c]
  • Использование понимания списка: [x for x in iterable]
  • Использование конструктора типов: list() или list(iterable)

Но кажется, что при использовании list() он использует больше памяти.

И чем больше list больше, разрыв увеличивается.

Разница в памяти

Почему это происходит?

ОБНОВЛЕНИЕ # 1

Тестирование с помощью Python 3.6.0b2:

Python 3.6.0b2 (default, Oct 11 2016, 11:52:53) 
[GCC 5.4.0 20160609] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import sys
>>> sys.getsizeof(list(range(100)))
1008
>>> sys.getsizeof([i for i in range(100)])
912

ОБНОВЛЕНИЕ # 2

Тест с Python 2.7.12:

Python 2.7.12 (default, Jul  1 2016, 15:12:24) 
[GCC 5.4.0 20160609] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import sys
>>> sys.getsizeof(list(xrange(100)))
1016
>>> sys.getsizeof([i for i in xrange(100)])
920

Ответ 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

Ответ 2

Спасибо всем за то, что помогли мне понять этот удивительный Python.

Я не хочу задавать вопрос, что массивный (вот почему я отправляю ответ), просто хочу показать и поделиться своими мыслями.

Как @ReutSharabani правильно отметил: "list() детерминистически определяет размер списка". Вы можете видеть это на этом графике.

graph of sizes

Когда вы append или используете понимание списка, у вас всегда есть какие-то границы, которые расширяются, когда вы достигаете какой-то точки. И с list() у вас почти одинаковые границы, но они плавают.

UPDATE

Итак, благодаря @ReutSharabani, @tavo, @SvenFestersen

Подводя итог: list() preallocates memory зависит от размера списка, понимание списка не может этого сделать (он запрашивает больше памяти, когда это необходимо, например .append()). Поэтому list() хранить больше памяти.

Еще один график, показывающий list() предопределить память. Таким образом, зеленая строка показывает list(range(830)) добавляющий элемент по элементу, а память не меняется.

list() preallocates memory

ОБНОВЛЕНИЕ 2

Как отметил @Barmar в комментариях ниже, list() должен меня быстрее, чем понимание списка, поэтому я провел timeit() с number=1000 для длины list от 4**0 до 4**10, а результаты

измерения времени