İki özdeş listenin neden farklı bellek alanı var?


155

İki liste oluşturdum l1ve l2her biri farklı bir oluşturma yöntemiyle:

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

Ama çıktı beni şaşırttı:

Size of l1 = 144
Size of l2 = 192

Liste kavrayışı ile oluşturulan liste bellekte daha büyüktür, ancak iki liste Python'da aynıdır.

Neden? Bu bazı CPython içsel bir şey mi, yoksa başka bir açıklama mı?


2
Muhtemelen, tekrarlama operatörü, temel diziyi tam olarak boyutlandıran bir işlevi çağıracaktır. Unutmayın ki 144 == sys.getsizeof([]) + 8*10)burada 8 bir işaretçi boyutudur.
juanpa.arrivillaga

1
Not değiştirmek eğer 10için 11, [None] * 11liste boyutuna sahip 152, ancak liste anlama hala boyutuna sahip 192. Önceden bağlantılı soru tam bir kopya değil, ancak bunun neden olduğunu anlamakla ilgilidir.
Patrick Haugh

Yanıtlar:


162

Yazarken [None] * 10, Python tam olarak 10 nesneden oluşan bir listeye ihtiyacı olacağını bilir, bu yüzden tam olarak bunu tahsis eder.

Bir liste kavrayışı kullandığınızda, Python ne kadarına ihtiyaç duyacağını bilmiyor. Böylece elementler eklendikçe liste yavaş yavaş büyür. Her yeniden tahsis için hemen ihtiyaç duyulandan daha fazla yer ayırır, böylece her eleman için yeniden tahsis etmek zorunda kalmaz. Sonuçta ortaya çıkan listenin gerekenden biraz daha büyük olması muhtemeldir.

Benzer boyutlarda oluşturulan listeleri karşılaştırırken bu davranışı görebilirsiniz:

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

İlk yöntemin sadece gerekli olanı tahsis ettiğini, ikincisinin ise periyodik olarak büyüdüğünü görebilirsiniz. Bu örnekte, 16 öğe için yeterli ayırma yapar ve 17. öğeye ulaştığında yeniden ayrılması gerekiyordu.


1
Evet, bu mantıklı. *Önde boyutu bildiğimde listeler oluşturmak daha iyi olur.
Andrej Kesely

27
@AndrejKesely Sadece listenizdeki [x] * ndeğişmez ile kullanın x. Sonuçta ortaya çıkan listede özdeş nesneye referanslar bulunur.
schwobaseggl

5
@schwobaseggl iyi, istediğiniz şey olabilir , ancak bunu anlamak iyidir.
juanpa.arrivillaga

19
@ juanpa.arrivillaga Doğru, olabilir. Ama genellikle değil ve özellikle SO, tüm verilerinin neden aynı anda değiştiğini merak eden posterlerle dolu: D
schwobaseggl

50

Bu soruda belirtildiği gibi , liste kavraması list.appendkaputun altında kullanır , bu nedenle aşırı konumlandırılan liste yeniden boyutlandırma yöntemini çağırır.

Bunu kendinize göstermek için aslında disdissasembler'ı kullanabilirsiniz :

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

Kod nesnesinin LIST_APPENDsökülmesinde opcode dikkat edin <listcomp>. Gönderen docs :

LIST_APPEND: (i)

Aramalar list.append(TOS[-i], TOS). Liste kavrayışlarını uygulamak için kullanılır.

Şimdi, liste tekrarlama işlemi için aşağıdakileri göz önüne alırsak neler olup bittiğine dair bir ipucumuz var:

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

Yani, boyutu tam olarak tahsis edebiliyor gibi görünüyor . Kaynak koduna baktığımızda, bunun tam olarak ne olduğunu görüyoruz:

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

Yani, burada: size = Py_SIZE(a) * n;. İşlevlerin geri kalanı diziyi doldurur.


"Bu soruda belirtildiği gibi liste-kavrama list.es başlık altında ekler kullanır" Bence bunu kullanmak daha doğru olduğunu düşünüyorum .extend().
Birikim

@Accumulation neden böyle düşünüyorsun?
juanpa.arrivillaga

Çünkü öğeleri tek tek eklemiyor. Bir listeye öğe eklediğinizde, gerçekten yeni bir bellek ayırma özelliğine sahip yeni bir liste oluşturursunuz ve listeyi bu yeni bellek ayırmaya koyarsınız. Öte yandan, liste kavrayışları, yeni öğelerin çoğunu önceden tahsis edilmiş belleğe yerleştirir ve tahsis edilen bellek bittiğinde, sadece yeni öğe için yeterli değil, başka bir bellek aynası tahsis ederler.
Birikim

7
@Accumulation Bu yanlış. list.appendamortize edilmiş sabit bir zaman işlemidir, çünkü bir liste yeniden boyutlandırıldığında fazla yer ayırır. Bu nedenle, her ekleme işlemi yeni atanan bir diziye neden olmaz. Her durumda bir soru ben aslında, liste comprehensions o kaynak kodunda gösterileri sizi bağlantılı olduğunu yapmak kullanımının list.append,. Birazdan dizüstü bilgisayarımda olacağım ve size bir liste kavrayışı ve ilgili LIST_APPENDopcode için demonte bayt kodunu gösterebilirim
juanpa.arrivillaga

3

Hiçbiri bir bellek bloğu değildir, ancak önceden belirlenmiş bir boyut değildir. Buna ek olarak, bir dizi içinde dizi öğeleri arasında fazladan boşluk vardır. Bunu çalıştırarak kendiniz görebilirsiniz:

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

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

Hangi toplamı l2 değil, daha azdır.

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

Ve bu, boyutunun onda birinden çok daha büyük l1.

Sayılarınız hem işletim sisteminizin ayrıntılarına hem de işletim sisteminizdeki geçerli bellek kullanımının ayrıntılarına bağlı olarak değişmelidir. [Hiçbiri] boyutu hiçbir zaman değişkenin saklanacağı bitişik bellekten daha büyük olamaz ve değişkenin daha sonra dinamik olarak daha büyük olarak ayrılması durumunda değişkenin taşınması gerekebilir.


1
Noneaslında temel alınan dizide saklanmaz, saklanan tek şey bir PyObjectişaretçi (8 bayt). Tüm Python nesneleri yığın üzerinde ayrılır. Nonebir singleton, bu nedenle birçok nones içeren bir listeye sahip olmanız, Noneöbek üzerinde aynı nesneye bir dizi PyObject işaretçisi oluşturacaktır (ve ek işlem başına ek bellek kullanmaz None). Ne demek istediğinizden emin değilim "Hiçbiri önceden belirlenmiş bir boyuta sahip değil", ama bu doğru gelmiyor. Son olarak, getsizeofher bir öğeyle olan döngünüz, gösterdiği düşünülen şeyleri göstermiyor.
juanpa.arrivillaga

Söylediğiniz gibi doğruysa, [Yok] * 10 boyutu [Yok] boyutuyla aynı olmalıdır. Ama açıkça bu böyle değil - bazı ekstra depolama alanı eklendi. Aslında, [Yok] 'un on katı (160) boyutu da [Yok]' un büyüklüğünün on ile çarpımından daha azdır. İşaret ettiğiniz gibi, [Yok] 'a işaretçi boyutu açıkça [Yok]' un kendisinden daha küçüktür (72 bayt yerine 16 bayt). Ancak, 160 + 32 192'dir. Önceki cevabın sorunu da tamamen çözdüğünü düşünmüyorum. Ekstra az miktarda bellek (belki de makine durumuna bağlı) tahsis edildiği açıktır.
StevenJD

"Söylediğiniz gibi doğruysa, [Hiçbiri] * 10 boyutu [Hiçbiri] ile aynı olmalıdır" diyorum ki bu muhtemelen bunu ima edebilir? Yine, altta yatan arabellek aşırı tahsis edildiğine veya listenin boyutunun altta yatan arabellek boyutundan daha fazlasını içerdiğine (elbette öyle) odaklanmış gibi görünüyorsunuz, ancak bu nokta değil bu soru. Yine, gestsizeofher elebirinde l2kullanımınız yanıltıcıdır, çünkü getsizeof(l2) kap içindeki elemanların boyutunu dikkate almaz .
juanpa.arrivillaga

Kendine bunu son iddiayı ispat etmek için yapmak l1 = [None]; l2 = [None]*100; l3 = [l2]o zaman print(sys.getsizeof(l1), sys.getsizeof(l2), sys.getsizeof(l3)). Eğer böyle bir sonuç elde edersiniz: 72 864 72. Kendisine, sırasıyla 64 + 1*8, 64 + 100*8ve 64 + 1*8, yine, 8 bayt ibre boyutu olan bir 64-bit sistem varsayılarak.
juanpa.arrivillaga

1
Belirttiğim gibi sys.getsizeof* kaptaki öğelerin boyutunu hesaba katmaz. Kaynaktan Dokümanlar "objeye ilişkilendirilen sadece bellek tüketimini nesnelerin değil hafıza kullanımı için hesaplanmaktadır bu için ... Bkz gelir özyinelemeli sizeof yinelemeli kapların büyüklüğü bulmak ve () getsizeof kullanarak örnek için tarif tüm içeriği. "
juanpa.arrivillaga
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.