A tuple
ocupa menos espaço de memória em Python:
>>> a = (1,2,3)
>>> a.__sizeof__()
48
enquanto list
s ocupa mais espaço na memória:
>>> b = [1,2,3]
>>> b.__sizeof__()
64
O que acontece internamente no gerenciamento de memória Python?
A tuple
ocupa menos espaço de memória em Python:
>>> a = (1,2,3)
>>> a.__sizeof__()
48
enquanto list
s ocupa mais espaço na memória:
>>> b = [1,2,3]
>>> b.__sizeof__()
64
O que acontece internamente no gerenciamento de memória Python?
Respostas:
Presumo que você esteja usando CPython e com 64 bits (obtive os mesmos resultados no meu CPython 2.7 de 64 bits). Pode haver diferenças em outras implementações Python ou se você tiver um Python de 32 bits.
Independentemente da implementação, list
s são de tamanho variável enquanto tuple
s são de tamanho fixo.
Assim, tuple
s podem armazenar os elementos diretamente dentro da estrutura; por outro lado, as listas precisam de uma camada de indireção (ela armazena um ponteiro para os elementos). Essa camada de indireção é um ponteiro, em sistemas de 64 bits que é 64 bits, portanto, 8 bytes.
Mas há outra coisa que eles list
fazem: eles alocam em excesso. Caso contrário, list.append
seria uma O(n)
operação sempre - para torná-la amortizada O(1)
(muito mais rápido !!!) ela superaloca. Mas agora ele precisa manter o controle do tamanho alocado e do tamanho preenchido (os tamanhos tuple
só precisam armazenar um tamanho, porque o tamanho alocado e preenchido são sempre idênticos). Isso significa que cada lista deve armazenar outro "tamanho" que em sistemas de 64 bits é um número inteiro de 64 bits, novamente 8 bytes.
Portanto, os list
s precisam de pelo menos 16 bytes a mais de memória do que os tuple
s. Por que eu disse "pelo menos"? Por causa da superalocação. Superalocação significa que aloca mais espaço do que o necessário. No entanto, a quantidade de superalocação depende de "como" você cria a lista e do histórico de acréscimo / exclusão:
>>> l = [1,2,3]
>>> l.__sizeof__()
64
>>> l.append(4) # triggers re-allocation (with over-allocation), because the original list is full
>>> l.__sizeof__()
96
>>> l = []
>>> l.__sizeof__()
40
>>> l.append(1) # re-allocation with over-allocation
>>> l.__sizeof__()
72
>>> l.append(2) # no re-alloc
>>> l.append(3) # no re-alloc
>>> l.__sizeof__()
72
>>> l.append(4) # still has room, so no over-allocation needed (yet)
>>> l.__sizeof__()
72
Resolvi criar algumas imagens para acompanhar a explicação acima. Talvez sejam úteis
É assim que (esquematicamente) é armazenado na memória em seu exemplo. Eu destaquei as diferenças com os ciclos vermelhos (mão livre):
Na verdade, isso é apenas uma aproximação porque os int
objetos também são objetos Python e o CPython até reutiliza pequenos inteiros, então uma representação provavelmente mais precisa (embora não tão legível) dos objetos na memória seria:
Links Úteis:
tuple
struct no repositório CPython para Python 2.7list
struct no repositório CPython para Python 2.7int
struct no repositório CPython para Python 2.7Observe que __sizeof__
realmente não retorna o tamanho "correto"! Ele apenas retorna o tamanho dos valores armazenados. No entanto, quando você usa, sys.getsizeof
o resultado é diferente:
>>> import sys
>>> l = [1,2,3]
>>> t = (1, 2, 3)
>>> sys.getsizeof(l)
88
>>> sys.getsizeof(t)
72
Existem 24 bytes "extras". Eles são reais , essa é a sobrecarga do coletor de lixo que não é considerada no __sizeof__
método. Isso porque geralmente você não deve usar métodos mágicos diretamente - use as funções que sabem como tratá-los, neste caso: sys.getsizeof
(que na verdade adiciona a sobrecarga de GC ao valor retornado de __sizeof__
).
list
alocação de memória stackoverflow.com/questions/40018398/…
list()
ou uma compreensão de lista.
Vou dar um mergulho mais profundo na base de código CPython para que possamos ver como os tamanhos são realmente calculados. Em seu exemplo específico , nenhuma superalocação foi realizada, então não tocarei nisso .
Vou usar valores de 64 bits aqui, como você.
O tamanho de list
s é calculado a partir da seguinte função list_sizeof
:
static PyObject *
list_sizeof(PyListObject *self)
{
Py_ssize_t res;
res = _PyObject_SIZE(Py_TYPE(self)) + self->allocated * sizeof(void*);
return PyInt_FromSsize_t(res);
}
Aqui Py_TYPE(self)
está uma macro que captura ob_type
de self
(retornando PyList_Type
) enquanto _PyObject_SIZE
outra macro captura tp_basicsize
desse tipo. tp_basicsize
é calculado como sizeof(PyListObject)
onde PyListObject
está a estrutura da instância.
A PyListObject
estrutura possui três campos:
PyObject_VAR_HEAD # 24 bytes
PyObject **ob_item; # 8 bytes
Py_ssize_t allocated; # 8 bytes
estes têm comentários (que cortei) explicando o que são, siga o link acima para lê-los. PyObject_VAR_HEAD
expande-se em três campos de 8 byte ( ob_refcount
, ob_type
e ob_size
) para uma 24
contribuição byte.
Então, por enquanto res
é:
sizeof(PyListObject) + self->allocated * sizeof(void*)
ou:
40 + self->allocated * sizeof(void*)
Se a instância da lista tiver elementos que estão alocados. a segunda parte calcula sua contribuição. self->allocated
, como o nome indica, contém o número de elementos alocados.
Sem quaisquer elementos, o tamanho das listas é calculado para ser:
>>> [].__sizeof__()
40
ou seja, o tamanho da estrutura da instância.
tuple
objetos não definem uma tuple_sizeof
função. Em vez disso, eles usam object_sizeof
para calcular seu tamanho:
static PyObject *
object_sizeof(PyObject *self, PyObject *args)
{
Py_ssize_t res, isize;
res = 0;
isize = self->ob_type->tp_itemsize;
if (isize > 0)
res = Py_SIZE(self) * isize;
res += self->ob_type->tp_basicsize;
return PyInt_FromSsize_t(res);
}
Isso, como para list
s, agarra o tp_basicsize
e, se o objeto tiver um diferente de zero tp_itemsize
(o que significa que ele tem instâncias de comprimento variável), ele multiplica o número de itens na tupla (pelos quais ele obtém via Py_SIZE
) tp_itemsize
.
tp_basicsize
novamente usa sizeof(PyTupleObject)
onde a PyTupleObject
estrutura contém :
PyObject_VAR_HEAD # 24 bytes
PyObject *ob_item[1]; # 8 bytes
Portanto, sem nenhum elemento (ou seja, Py_SIZE
retornos 0
), o tamanho das tuplas vazias é igual a sizeof(PyTupleObject)
:
>>> ().__sizeof__()
24
Hã? Bem, aqui está uma esquisitice para a qual não encontrei uma explicação, o tp_basicsize
de tuple
s é calculado da seguinte maneira:
sizeof(PyTupleObject) - sizeof(PyObject *)
por que um 8
byte adicional é removido tp_basicsize
é algo que não consegui descobrir. (Veja o comentário de MSeifert para uma possível explicação)
Mas, essa é basicamente a diferença em seu exemplo específico . list
s também mantêm vários elementos alocados que ajudam a determinar quando alocar em excesso novamente.
Agora, quando elementos adicionais são adicionados, as listas de fato realizam essa superalocação para obter anexos O (1). Isso resulta em tamanhos maiores, pois o de MSeifert cobre bem em sua resposta.
ob_item[1]
é principalmente um espaço reservado (então faz sentido que seja subtraído do tamanho básico). O tuple
é alocado usando PyObject_NewVar
. Eu não descobri os detalhes, então isso é apenas um palpite ...
A resposta da MSeifert cobre isso amplamente; para mantê-lo simples, você pode pensar em:
tuple
é imutável. Depois de definido, você não pode alterá-lo. Portanto, você sabe com antecedência quanta memória precisa alocar para esse objeto.
list
é mutável. Você pode adicionar ou remover itens de ou para ele. Tem que saber o tamanho dele (para impl. Interno). Ele é redimensionado conforme necessário.
Não há refeições gratuitas - esses recursos têm um custo. Daí a sobrecarga de memória para listas.
O tamanho da tupla é prefixado, o que significa que na inicialização da tupla o interpretador aloca espaço suficiente para os dados contidos, e ponto final, dando-lhe imutável (não pode ser modificado), enquanto uma lista é um objeto mutável, portanto, implicando dinâmico alocação de memória, para evitar a alocação de espaço cada vez que você anexar ou modificar a lista (alocar espaço suficiente para conter os dados alterados e copiar os dados para eles), ele aloca espaço adicional para anexos futuros, modificações, ... resume tudo.