web-dev-qa-db-fra.com

Pourquoi deux listes identiques ont une empreinte mémoire différente?

J'ai créé deux listes l1 et l2, mais chacune avec une méthode de création différente:

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

Mais la sortie m'a surpris:

Size of l1 = 144
Size of l2 = 192

La liste créée avec une compréhension de liste est une plus grande taille en mémoire, mais les deux listes sont identiques dans Python sinon.

Pourquoi donc? Est-ce quelque chose interne à CPython ou une autre explication?

146
Andrej Kesely

Lorsque vous écrivez [None] * 10, Python sait qu'il aura besoin d'une liste contenant exactement 10 objets. Il alloue donc exactement cette information.

Lorsque vous utilisez une liste de compréhension, Python ne sait pas combien de temps il lui faudra. Donc, la liste grandit au fur et à mesure que des éléments sont ajoutés. Pour chaque réaffectation, il alloue plus de place que nécessaire, de sorte qu'il ne soit pas nécessaire de réaffecter chaque élément. La liste obtenue sera probablement un peu plus longue que nécessaire.

Vous pouvez constater ce comportement lors de la comparaison de listes créées avec des tailles similaires:

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

Vous pouvez voir que la première méthode alloue exactement ce qui est nécessaire, tandis que la seconde augmente périodiquement. Dans cet exemple, il alloue assez de ressources pour 16 éléments et doit être réaffecté pour atteindre le 17e.

157
interjay

Comme indiqué dans cette question la compréhension de liste utilise list.append sous le capot, elle appellera donc la méthode list-redimensionner, qui sur-attribue.

Pour vous en convaincre, vous pouvez utiliser le dissasembleur dis:

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

Notez que l'opcode LIST_APPEND dans le désassemblage de l'objet de code <listcomp>. De la docs :

LIST_APPEND (i)

Appelle list.append(TOS[-i], TOS). Utilisé pour implémenter les compréhensions de liste.

Maintenant, pour l'opération de répétition de liste, nous avons un indice sur ce qui se passe si nous considérons:

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

Ainsi, il semble pouvoir exactement allouer la taille. En regardant code source , on voit que c'est exactement ce qui se passe:

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

À savoir ici: size = Py_SIZE(a) * n;. Le reste des fonctions remplit simplement le tableau.

47
juanpa.arrivillaga

Aucun n'est un bloc de mémoire, mais ce n'est pas une taille pré-spécifiée. En plus de cela, il y a un espacement supplémentaire dans un tableau entre les éléments du tableau. Vous pouvez le voir vous-même en lançant:

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

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

Qui ne totalise pas la taille de l2, mais plutôt moins.

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

Et cela est beaucoup plus grand qu'un dixième de la taille de l1.

Vos chiffres devraient varier en fonction des détails de votre système d'exploitation et des détails de l'utilisation actuelle de la mémoire dans votre système d'exploitation. La taille de [Aucun] ne peut jamais être supérieure à la mémoire adjacente disponible dans laquelle la variable est configurée pour être stockée. Il peut être nécessaire de déplacer la variable si elle est allouée dynamiquement ultérieurement pour être plus grande.

3
StevenJD