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 

Из документов :

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

  • Использование пары квадратных скобок для обозначения пустого списка: []
  • Использование квадратных скобок, разделение элементов запятыми: [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 

Я думаю, что вы видите шаблоны перераспределения, это образец из источника :

 /* 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 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 

Спасибо всем за то, что помогли мне понять этот потрясающий Python.

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

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

график размеров

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

ОБНОВИТЬ

Так спасибо @ReutSharabani , @tavo , @SvenFestersen

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

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

list () предопределяет память

ОБНОВЛЕНИЕ 2

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

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