python management Por que duas listas idênticas têm um footprint de memória diferente?




python memory module (3)

Quando você escreve [None] * 10 , o Python sabe que precisará de uma lista de exatamente 10 objetos, por isso aloca exatamente isso.

Quando você usa uma compreensão de lista, o Python não sabe quanto precisará. Por isso, cresce gradualmente a lista à medida que os elementos são adicionados. Para cada realocação, ele aloca mais espaço do que o necessário imediatamente, para que ele não precise realocar para cada elemento. A lista resultante provavelmente será um pouco maior que o necessário.

Você pode ver esse comportamento ao comparar listas criadas com tamanhos semelhantes:

>>> sys.getsizeof([None]*15)
184
>>> sys.getsizeof([None]*16)
192
>>> sys.getsizeof([None for _ in range(15)])
192
>>> sys.getsizeof([None for _ in range(16)])
192
>>> sys.getsizeof([None for _ in range(17)])
264

Você pode ver que o primeiro método aloca exatamente o que é necessário, enquanto o segundo cresce periodicamente. Neste exemplo, ele aloca o suficiente para 16 elementos e teve que realocar ao atingir o 17º.

Eu criei duas listas l1 e l2 , mas cada uma com um método de criação diferente:

import sys

l1 = [None] * 10
l2 = [None for _ in range(10)]

print('Size of l1 =', sys.getsizeof(l1))
print('Size of l2 =', sys.getsizeof(l2))

Mas a saída me surpreendeu:

Size of l1 = 144
Size of l2 = 192

A lista criada com uma compreensão de lista é um tamanho maior na memória, mas as duas listas são idênticas no Python.

Por que é que? Isso é alguma coisa interna CPython, ou alguma outra explicação?


Nenhum é um bloco de memória, mas não é um tamanho pré-especificado. Além disso, há algum espaçamento extra em uma matriz entre os elementos da matriz. Você mesmo pode ver isso executando:

for ele in l2:
    print(sys.getsizeof(ele))

>>>>16
16
16
16
16
16
16
16
16
16

Qual não totaliza o tamanho de l2, mas é menor.

print(sys.getsizeof([None]))
72

E isso é muito maior que um décimo do tamanho de l1 .

Seus números devem variar dependendo dos detalhes do sistema operacional e dos detalhes do uso de memória atual no sistema operacional. O tamanho de [Nenhum] nunca pode ser maior que a memória adjacente disponível onde a variável está definida para ser armazenada, e a variável pode ter que ser movida se posteriormente for dinamicamente alocada para ser maior.


Como foi observado nesta pergunta, a compreensão da lista usa list.append sob o capô, então ele chamará o método de redimensionamento de lista, que é superalocado.

Para demonstrar isso a si mesmo, você pode realmente usar o dis dissasembler:

>>> code = compile('[x for x in iterable]', '', 'eval')
>>> import dis
>>> dis.dis(code)
  1           0 LOAD_CONST               0 (<code object <listcomp> at 0x10560b810, file "", line 1>)
              2 LOAD_CONST               1 ('<listcomp>')
              4 MAKE_FUNCTION            0
              6 LOAD_NAME                0 (iterable)
              8 GET_ITER
             10 CALL_FUNCTION            1
             12 RETURN_VALUE

Disassembly of <code object <listcomp> at 0x10560b810, file "", line 1>:
  1           0 BUILD_LIST               0
              2 LOAD_FAST                0 (.0)
        >>    4 FOR_ITER                 8 (to 14)
              6 STORE_FAST               1 (x)
              8 LOAD_FAST                1 (x)
             10 LIST_APPEND              2
             12 JUMP_ABSOLUTE            4
        >>   14 RETURN_VALUE
>>>

Observe o código de operação LIST_APPEND na desmontagem do objeto de código <listcomp> . Dos docs :

LIST_APPEND (i)

Chama a list.append(TOS[-i], TOS) . Usado para implementar as compreensões da lista.

Agora, para a operação de repetição de lista, temos uma sugestão sobre o que está acontecendo se considerarmos:

>>> import sys
>>> sys.getsizeof([])
64
>>> 8*10
80
>>> 64 + 80
144
>>> sys.getsizeof([None]*10)
144

Então, parece ser capaz de alocar exatamente o tamanho. Olhando para o código fonte , vemos exatamente o que acontece:

static PyObject *
list_repeat(PyListObject *a, Py_ssize_t n)
{
    Py_ssize_t i, j;
    Py_ssize_t size;
    PyListObject *np;
    PyObject **p, **items;
    PyObject *elem;
    if (n < 0)
        n = 0;
    if (n > 0 && Py_SIZE(a) > PY_SSIZE_T_MAX / n)
        return PyErr_NoMemory();
    size = Py_SIZE(a) * n;
    if (size == 0)
        return PyList_New(0);
    np = (PyListObject *) PyList_New(size);

Ou seja, aqui: size = Py_SIZE(a) * n; . O resto das funções simplesmente preenche o array.







python-internals