Почему одинаковые списки при разных способах создания весят по разному?
Столкнулся я недавно с такой вот задачкой:
sys.getsizeof([0] * 3) # 80
sys.getsizeof([0, 0, 0]) # 120
sys.getsizeof([0 for i in range(3)]) # 88
Проверено на
Python 3.10.4
. Важно уточнить что такое поведение свойственно не всем версиям пайтона, а только приблизительно с3.9.19
до3.10.14
(но это не точно).
Собственно вопрос заключается в том что здесь происходит и почему одинаковые списки весят по разному?
import sys
list_1 = [0] * 3
list_2 = [0, 0, 0]
list_3 = [0 for i in range(3)]
print(list_1) # > [0, 0, 0]
print(list_2) # > [0, 0, 0]
print(list_3) # > [0, 0, 0]
print(list_1 == list_2 and list_2 == list_3 and list_3 == list_1) # > True
print(sys.getsizeof(list_1)) # > 80
print(sys.getsizeof(list_2)) # > 120
print(sys.getsizeof(list_3)) # > 88
Логично предположить, что первый массив после дублирования хранит три ссылки на один и тот же объект в памяти, и это как-то оптимизируется интерпретатором. Но разве для других массивов не происходит то же самое? Ведь Python кэширует числа от -5 до 256, а значит, и в других списках должны храниться три одинаковые ссылки на кэшированное число 0. Однако тогда возникает вопрос: почему три ссылки на одинаковые числа весят столько же, сколько и три ссылки на разные числа, и при этом меньше, чем в списках 2 и 3?
list_4 = list(range(3))
print(list_4) # > [0, 1, 2]
print(sys.getsizeof(list_4)) # > 80
Ответы (1 шт):
[0] * <константа>
- строит список точно заказанного размера.[0, 0, 0, ...]
- строит список одним вызовомlist.extend
, который для производительности выделяет место с запасом.[0 for i in range(<константа)]
- строит список вызываяlist.append
в цикле. И снова ради производительности место выделяется с запасом. Но этот запас считается слегка по-другому.
Подробности ниже.
[0] * <константа>
Выражение [0] * <константа>
всегда компилируется одинаково и приводит к вызову BINARY_MULTIPLY
:
n | размер | код | ассемблер |
---|---|---|---|
... | ... | ... | ... |
4 | 88 | [0] * 4 |
2 0 LOAD_CONST 1 (0) |
... | ... | ... | ... |
BINARY_MULTIPLY
с приключениями приходит в
list_repeat
, который выделяет список ровно требуемого размера (n) и заполняет его нулями. То есть, размер списка всегда 56 + 8n.
n | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
---|---|---|---|---|---|---|---|---|---|---|---|
размер | 56 | 64 | 72 | 80 | 88 | 96 | 104 | 112 | 120 | 128 | 136 |
Литерал [0, 0, 0, ...]
Литералы компилируются по разному в зависимости от числа нулей:
n | размер | код | ассемблер |
---|---|---|---|
0 | 56 | [] |
2 0 BUILD_LIST 0 |
1 | 64 | [0] |
2 0 LOAD_CONST 1 (0) |
2 | 72 | [0, 0] |
2 0 LOAD_CONST 1 (0) |
3 | 120 | [0, 0, 0] |
2 0 BUILD_LIST 0 |
4 | 120 | [0, 0, 0, 0] |
2 0 BUILD_LIST 0 |
5 | 120 | [0, 0, 0, 0, 0] |
2 0 BUILD_LIST 0 |
... | ... | ... | ... |
[]
, [0]
и [0, 0]
обрабатываются вызовом BUILD_LIST
, который строит точный список. В остальных случаях компилятор сохраняет кортеж (не список (!)) с нулями и вызывает LIST_EXTEND
на пустом списке.
LIST_EXTEND
- процедура общего назначения. Она должна работать быстро, если вы вызываете её в цикле и добавляете данные в список небольшими порциями. Можно показать что если она будет выделять место под элементы точно, добавление начнёт тормозить на больших списках. Так что список растёт шагами, примерно пропорциональными его размеру. Путанный код внутри list_resize
отвечает за странные скачки. Обратите внимание на 120 после 152:
n | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
---|---|---|---|---|---|---|---|---|---|---|---|
размер | 56 | 64 | 72 | 120 | 120 | 120 | 152 | 120 | 120 | 152 | 152 |
[0 for i in range(<константа)]
Компилятор одинаково обрабатывает все константы. Например [0 for i in range(4)]
:
2 0 LOAD_CONST 1 (<code object at 0x7f71c70b7260, file "", line 2>)
2 LOAD_CONST 2 ('comprehension_4..')
4 MAKE_FUNCTION 0
6 LOAD_GLOBAL 0 (range)
8 LOAD_CONST 3 (4)
10 CALL_FUNCTION 1
12 GET_ITER
14 CALL_FUNCTION 1
16 RETURN_VALUE
Disassembly of <code object at 0x7f71c70b7260, file "", line 2>:
2 0 BUILD_LIST 0
2 LOAD_FAST 0 (.0)
>> 4 FOR_ITER 4 (to 14)
6 STORE_FAST 1 (i)
8 LOAD_CONST 0 (0)
10 LIST_APPEND 2
12 JUMP_ABSOLUTE 2 (to 4)
>> 14
Создаётся пустой список и дополняется нулями (LIST_APPEND
). Размер списка заранее не вычисляется, список растёт прямо в процессе. Снова он растёт скачками, иначе будут тормоза. Снова за всё отвечает путанный код в list_resize
. Но тут он вызывается для других параметров и приводит к немного другим размерам:
n | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
---|---|---|---|---|---|---|---|---|---|---|---|
размер | 56 | 88 | 88 | 88 | 88 | 120 | 120 | 120 | 120 | 184 | 184 |
NB По производительности этот вариант самый худший: множественные перевыделения списка, явный цикл на Питоне. Просто кошмар!