Tuplelar neden bellekte listelerden daha az yer kaplar?


105

A tuple, Python'da daha az bellek alanı kaplar:

>>> a = (1,2,3)
>>> a.__sizeof__()
48

oysa lists daha fazla bellek alanı kaplar:

>>> b = [1,2,3]
>>> b.__sizeof__()
64

Python bellek yönetiminde dahili olarak ne olur?


1
Bunun dahili olarak nasıl çalıştığından emin değilim, ancak liste nesnesi en azından örneğin tuple'ın sahip olmadığı ek gibi daha fazla işleve sahiptir. Bu nedenle, daha basit bir nesne türü olarak tuple'ın daha küçük olması
mantıklıdır

Sanırım bu aynı zamanda makineden makineye de bağlı .... benim için a = (1,2,3) 72 alır ve b = [1,2,3] 88 alır.
Amrit 10'17

6
Python tupleları değişmezdir. Değişken nesneler, çalışma zamanı değişiklikleriyle başa çıkmak için fazladan ek yüke sahiptir.
Lee Daniel Crocker

@ Metar Bir türün sahip olduğu yöntem sayısı, örneklerin aldığı bellek alanını etkilemez. Yöntem listesi ve kodları, nesne prototipi tarafından işlenir, ancak örnekler yalnızca verileri ve dahili değişkenleri depolar.
jjmontes

Yanıtlar:


144

CPython kullandığınızı ve 64 bit kullandığınızı varsayıyorum (CPython 2.7 64-bit'imde aynı sonuçları aldım). Diğer Python uygulamalarında veya 32 bit Python'unuz varsa farklılıklar olabilir.

Ne olursa olsun uygulama, lists değişken büyüklükte ise tuples sabit boyuttadır.

Böylece tuples öğeleri doğrudan yapının içinde depolayabilir, diğer yandan listeler bir yönlendirme katmanına ihtiyaç duyar (öğelere bir işaretçi depolar). Bu yönlendirme katmanı, 64 bitlik sistemlerde 64 bit, dolayısıyla 8 bayt olan bir işaretleyicidir.

Ama bu başka bir şey var listOnlar aşırı tahsis: s yok. Aksi takdirde list.append, her zaman bir O(n)operasyon olurdu - onu amorti etmek (çok daha hızlı !!!) için fazla tahsis eder. Ancak şimdi tahsis edilen boyutu ve doldurulan boyutu takip etmesi gerekir ( yalnızca bir boyut saklaması gerekir, çünkü tahsis edilen ve doldurulan boyut her zaman aynıdır). Bu, her listenin 64 bitlik sistemlerde yine 8 bayt olmak üzere 64 bitlik bir tam sayı olan başka bir "boyut" depolaması gerektiği anlamına gelir.O(1)tuple

So lists ihtiyacı en az 16 den fazla bellek bayt tuples. Neden "en azından" dedim? Fazla tahsis yüzünden. Aşırı tahsis, gerekenden daha fazla alan tahsis ettiği anlamına gelir. Ancak, fazla tahsis miktarı, listeyi "nasıl" oluşturduğunuza ve ekleme / silme geçmişine bağlıdır:

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

Görüntüler

Yukarıdaki açıklamaya eşlik edecek bazı resimler oluşturmaya karar verdim. Belki bunlar yardımcı olur

Örneğinizde hafızada (şematik olarak) bu şekilde saklanır. Kırmızı (serbest el) döngülerdeki farklılıkları vurguladım:

görüntü açıklamasını buraya girin

Bu aslında sadece bir yaklaşımdır çünkü intnesneler aynı zamanda Python nesneleridir ve CPython küçük tam sayıları yeniden kullanır, dolayısıyla bellekteki nesnelerin muhtemelen daha doğru bir temsili (okunabilir olmasa da) şöyle olacaktır:

görüntü açıklamasını buraya girin

Kullanışlı bağlantılar:

Bunun __sizeof__gerçekten "doğru" boyutu döndürmediğini unutmayın ! Yalnızca saklanan değerlerin boyutunu döndürür. Ancak kullandığınızda sys.getsizeofsonuç farklıdır:

>>> import sys
>>> l = [1,2,3]
>>> t = (1, 2, 3)
>>> sys.getsizeof(l)
88
>>> sys.getsizeof(t)
72

24 "ekstra" bayt vardır. Bunlar gerçektir , __sizeof__yöntemde hesaba katılmayan çöp toplayıcı ek yükü budur . Bunun nedeni, genellikle sihirli yöntemleri doğrudan kullanmamanız gerektiğidir - bunları nasıl işleyeceğinizi bilen işlevleri kullanın, bu durumda: sys.getsizeof(aslında GC ek yükünü döndürülen değere ekler__sizeof__ ).


Re " Yani listelerin tuple'lerden en az 16 bayt daha fazla belleğe ihtiyacı var. ", Bu 8 olmaz mıydı? Listeler için bir boyut ve listeler için iki boyut, listeler için bir ekstra boyut anlamına gelir.
ikegami

1
Evet, listenin fazladan bir "boyutu" (8 bayt) vardır, ancak bunları doğrudan yapıda depolamak yerine (dizinin yaptığı gibi) "PyObject dizileri" için bir işaretçi (8 bayt) depolar. 8 + 8 = 16.
MSeifert

2
Bellek ayırma hakkında başka bir yararlı bağlantı stackoverflow.com/questions/40018398/…list
vishes_shell

@vishes_shell Bu, soruyla gerçekten alakalı değil çünkü sorudaki kod fazla tahsis etmiyor . Ancak evet, kullanım sırasında fazla tahsis miktarı list()veya bir liste anlayışı hakkında daha fazla bilgi edinmek istemeniz durumunda yararlıdır .
MSeifert

1
@ user3349993 Tuplelar değişmezdir, bu yüzden bir demete ekleyemezsiniz veya bir demetten bir öğeyi kaldıramazsınız.
MSeifert

31

Boyutların gerçekte nasıl hesaplandığını görebilmemiz için CPython kod tabanına daha derin bir dalış yapacağım. Belirli örnekte , bu konuda dokunmayacağım Böylece yüksek tahsisleri, yapılmamıştır .

Burada sizin gibi 64 bitlik değerler kullanacağım.


listS için boyut aşağıdaki işlevden hesaplanır 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);
}

İşte Py_TYPE(self)çekecek bir makro olduğu ob_typebir self(dönen PyList_Type) ise _PyObject_SIZEbaşka bir makro olduğunu kıskaçlar tp_basicsizebu tip. örnek yapısının nerede olduğu tp_basicsizeolarak hesaplanır .sizeof(PyListObject)PyListObject

PyListObjectYapı üç alanı vardır:

PyObject_VAR_HEAD     # 24 bytes 
PyObject **ob_item;   #  8 bytes
Py_ssize_t allocated; #  8 bytes

bunların ne olduklarını açıklayan yorumları (kırptığım) var, bunları okumak için yukarıdaki bağlantıyı takip edin. PyObject_VAR_HEADüç 8 baytlık alana ( ob_refcount, ob_typeve ob_size) genişler , böylece bir 24bayt katkısı olur.

Yani resşimdilik:

sizeof(PyListObject) + self->allocated * sizeof(void*)

veya:

40 + self->allocated * sizeof(void*)

Liste örneğinde ayrılmış öğeler varsa. ikinci bölüm katkılarını hesaplar. self->allocated, adından da anlaşılacağı gibi, tahsis edilen elemanların sayısını tutar.

Herhangi bir öğe olmaksızın, listelerin boyutu şu şekilde hesaplanır:

>>> [].__sizeof__()
40

yani örnek yapısının boyutu.


tuplenesneler bir tuple_sizeofişlevi tanımlamaz . Bunun yerine, object_sizeofboyutlarını hesaplamak için kullanırlar :

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);
}

Bu, lists için olduğu gibi , tp_basicsizeve eğer nesne sıfırdan farklıysa tp_itemsize(değişken uzunluklu örneklere sahipse), tuple'daki (üzerinden aldığı Py_SIZE) öğelerin sayısını çarpar tp_itemsize.

tp_basicsizeyapının içerdiğisizeof(PyTupleObject) yeri tekrar kullanır :PyTupleObject

PyObject_VAR_HEAD       # 24 bytes 
PyObject *ob_item[1];   # 8  bytes

Dolayısıyla, herhangi bir öğe olmadan (yani, Py_SIZEdöndürür 0) boş tupleların boyutu şuna eşittir sizeof(PyTupleObject):

>>> ().__sizeof__()
24

ha? Pekala, burada bir açıklama bulamadığım bir tuhaflık var tp_basicsize, tuples aslında şu şekilde hesaplanıyor:

sizeof(PyTupleObject) - sizeof(PyObject *)

neden ek 8baytların kaldırıldığını tp_basicsizeöğrenemediğim bir şey. (Olası bir açıklama için MSeifert'in yorumuna bakın)


Ancak, bu temelde sizin özel örneğinizdeki farktır . lists ayrıca, yeniden ne zaman fazla tahsis edileceğini belirlemeye yardımcı olan bir dizi tahsis edilmiş öğeyi de tutar.

Şimdi, ek elemanlar eklendiğinde, listeler gerçekten de O (1) uzantılarını elde etmek için bu fazla tahsisi gerçekleştirir. Bu, MSeifert'in cevabında güzelce örtüştüğü için daha büyük boyutlarla sonuçlanır.


Sanırım ob_item[1]çoğunlukla bir yer tutucu (bu yüzden temel boyuttan çıkarılması mantıklı). Kullanılarak tupletahsis edilir PyObject_NewVar. Ayrıntıları
çözmedim

@MSeifert Bunun için üzgünüz, düzeltildi :-). Gerçekten bilmiyorum, geçmişte bir noktada bulduğumu hatırlıyorum ama asla çok fazla ilgi göstermiyorum, belki gelecekte bir noktada bir soru sorarım :-)
Dimitris Fasarakis Hilliard

29

MSeifert cevabı bunu genel olarak kapsar; basit tutmak için şunları düşünebilirsiniz:

tupledeğişmezdir. Bir kez ayarlandıktan sonra değiştiremezsiniz. Böylece o nesne için ne kadar bellek ayırmanız gerektiğini önceden bilirsiniz.

listdeğiştirilebilir. İçine veya oradan öğe ekleyebilir veya çıkarabilirsiniz. Bunun boyutunu bilmek zorundadır (iç uygulama için). Gerektiği gibi yeniden boyutlandırılır.

Bedava yemek yoktur - bu yetenekler bir ücrete tabidir. Bu nedenle listeler için hafızadaki ek yük.


3

Tuple'ın boyutu önceden eklenmiştir, yani tuple başlatıldığında yorumlayıcı içerilen veriler için yeterli alan ayırır ve bu onun sonudur, değişmez verir (değiştirilemez), oysa bir liste değişken bir nesnedir, dolayısıyla dinamiktir. bellek tahsisi, böylece listeyi her eklediğinizde veya değiştirdiğinizde alan tahsis etmekten kaçınmak için (değiştirilen verileri içermek ve verileri ona kopyalamak için yeterli alan ayırın), gelecekteki ekleme, değişiklikler vb. için ek alan ayırır. özetliyor.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.