web-dev-qa-db-ja.com

2つの同じリストが異なるメモリフットプリントを持つのはなぜですか?

私は2つのリストl1l2を作成しましたが、それぞれが異なる作成方法を持っています:

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

しかし、出力は私を驚かせました:

Size of l1 = 144
Size of l2 = 192

リスト内包表記を使用して作成されたリストはメモリ内でより大きなサイズになりますが、それ以外の2つのリストはPythonでは同じです。

何故ですか?これはCPythonの内部的なものなのか、それとも他の説明なのか?

139
Andrej Kesely

[None] * 10を書くとき、Pythonはそれがちょうど10個のオブジェクトのリストを必要とすることを知っているので、それは正確にそれを割り当てます。

リスト内包表記を使用する場合、Pythonはそれが必要とする量を知りません。そのため、要素が追加されるにつれてリストは徐々に大きくなります。再割り当てごとに、すぐに必要とされるよりも多くのスペースが割り当てられるため、各要素ごとに再割り当てする必要はありません。結果のリストは、必要以上に大きくなる可能性があります。

同じようなサイズで作成されたリストを比較すると、この動作が見られます。

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

最初の方法では必要なものだけが割り当てられ、2番目の方法では定期的に成長することがわかります。この例では、16個の要素に対して十分な割り当てを行い、17日に達したときに再割り当てする必要がありました。

149
interjay

この質問 で述べたように、リスト内包表記は内部でlist.appendを使用しているので、リストのサイズを変更するメソッドを呼び出します。

これを自分で実証するために、実際にはdis disasemblerを使用できます。

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

LIST_APPENDコードオブジェクトの逆アセンブリの<listcomp>オペコードに注目してください。 のドキュメントから

LIST_APPEND(i)

list.append(TOS[-i], TOS)を呼び出します。リスト内包表記を実装するために使用されます。

さて、リスト反復操作については、考えてみると何が起こっているのかについてのヒントがあります。

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

だから、それは正確にサイズを割り当てることができるようです。 ソースコード を見ると、これがまさに起こることです。

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

つまり、ここでsize = Py_SIZE(a) * n;です。残りの関数は単純に配列を埋めます。

45

Noneはメモリブロックですが、事前に指定されたサイズではありません。それに加えて、配列要素の間には配列内に余分なスペースがあります。あなたはこれを実行することで自分で見ることができます:

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

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

これはl2の合計サイズではなく、むしろ小さいです。

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

そしてこれはl1のサイズの10分の1よりはるかに大きいです。

番号は、オペレーティングシステムの詳細とオペレーティングシステムの現在のメモリ使用量の詳細の両方によって異なります。 [None]のサイズは、変数が格納されるように設定されている使用可能な隣接メモリより大きくなることはありません。また、後で動的に割り当てられる場合は、変数を移動する必要があります。

3
StevenJD