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


155

私は2つのリストl1とを作成しましたl2が、それぞれに異なる作成方法を使用しています。

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の内部的なものですか、それとも他の説明ですか?


2
おそらく、繰り返し演算子は、基になる配列のサイズを正確に決定する関数を呼び出します。。なお、144 == sys.getsizeof([]) + 8*10)図8は、ポインタのサイズです。
juanpa.arrivillaga

1
に変更10した11場合、[None] * 11リストにはサイズ152がありますが、リストの内包にはまだサイズがあり192ます。以前にリンクされた質問は完全に重複しているわけではありませんが、これがなぜ起こるかを理解する上で重要です。
Patrick Haugh、2018

回答:


162

を記述すると[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番目に達すると再割り当てする必要がありました。


1
はい、それは理にかなっています。*前のサイズがわかっている場合は、おそらくリストを作成する方が良いでしょう。
Andrej Kesely

27
@AndrejKesely リスト内の[x] * n不変xでのみ使用します。結果のリストは、同じオブジェクトへの参照を保持します。
schwobaseggl

5
@schwobasegglまあ、それあなたが望むものかもしれませんが、それを理解するのは良いことです。
juanpa.arrivillaga

19
@ juanpa.arrivillaga確かにそうかもしれません。しかし、通常はそうではなく、特にSOは、すべてのデータが同時に変更された理由を不思議に思っているポスターでいっぱいです:D
schwobaseggl

50

この質問で述べたように、list-comprehensionはlist.append内部で使用しているので、overallocateするlist-resizeメソッドを呼び出します。

これを実際に示すために、実際に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
>>>

コードオブジェクトの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;。残りの関数は単に配列を埋めます。


「この質問で述べたように、list-comprehensionは内部でlist.appendを使用しています」を使用していると言う方がより正確だと思います.extend()
2018

@蓄積なぜそう信じますか?
juanpa.arrivillaga

要素を1つずつ追加していないためです。リストに要素を追加すると、実際には新しいメモリ割り当てで新しいリストが作成され、その新しいメモリ割り当てにリストが配置されます。一方、リスト内包表記は、すでに割り当てられているメモリに新しい要素のほとんどを配置し、割り当てられたメモリが不足すると、新しい要素に十分ではなく、別のメモリを割り当てます。
2018

7
@ Accumulation不正解です。list.appendリストのサイズが変更されると、それが全体に割り当てられるため、償却定時間操作です。したがって、すべての追加操作が、新しく割り当てられた配列になるわけではありません。いずれにせよ、私がリンクした質問は、実際にはリスト内包表記使用するソースコードであなたを示していますlist.append。私はLIST_APPEND
すぐに

3

どれもメモリのブロックではありませんが、事前に指定されたサイズではありません。それに加えて、配列要素間の配列には余分な間隔があります。次のコマンドを実行すると、これを自分で確認できます。

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

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

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

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

そして、これはのサイズの1/10をはるかに超えていl1ます。

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


1
None実際には基になる配列に格納されていませんPyObject。格納されるのはポインター(8バイト)だけです。すべてのPythonオブジェクトはヒープに割り当てられます。Noneはシングルトンなので、多くのなしのリストがあるとNone、ヒープ上の同じオブジェクトへのPyObjectポインタの配列が作成されます(追加のプロセスごとに追加のメモリを使用しませんNone)。「事前に指定されたサイズはありません」という意味がわかりませんが、正確ではありません。最後に、getsizeof各要素のループは、それが示していると思われるように見えるものを示していません。
juanpa.arrivillaga 2018

あなたが言う通りの場合、[なし] * 10のサイズは[なし]のサイズと同じでなければなりません。しかし、明らかにこれはそうではありません。追加のストレージが追加されています。実際、[None]のサイズは10回繰り返され(160)、[None]のサイズに10を掛けた値よりも小さくなります。ご指摘のとおり、[なし]へのポインターのサイズは[なし]自体のサイズよりも小さい(72バイトではなく16バイト)。ただし、160 + 32は192です。上記の回答で問題が完全に解決されるとは思いません。余分な少量のメモリ(おそらくマシンの状態に依存)が割り当てられていることは明らかです。
StevenJD 2018

「あなたの言うとおりの場合、[なし] * 10のサイズは[なし]のサイズと同じである必要があります」何を意味しているのでしょうか。繰り返しになりますが、基礎となるバッファーが過剰に割り当てられている、またはリストのサイズに基礎となるバッファーのサイズよりも多く含まれている(もちろんそうです)ことに集中しているようですが、それは重要ではありません。この質問。繰り返しになりますが、ではコンテナ内の要素のサイズが考慮されていないためgestsizeof、それぞれeleでのの使用l2は誤解を招く可能性getsizeof(l2) があります
juanpa.arrivillaga

最後の主張を自分で証明するにはl1 = [None]; l2 = [None]*100; l3 = [l2]、次に行いprint(sys.getsizeof(l1), sys.getsizeof(l2), sys.getsizeof(l3))ます。次のような結果が得られます72 864 72。すなわち、それぞれ64 + 1*864 + 100*8、および64 + 1*8、再び、8バイトのポインタサイズを有する64ビットのシステムを想定。
juanpa.arrivillaga

1
すでに述べたように、sys.getsizeof*はコンテナ内のアイテムのサイズを考慮していません。ドキュメントから:「オブジェクトに直接起因するメモリ消費のみが考慮され、オブジェクトが参照するオブジェクトのメモリ消費は考慮されません... コンテナのサイズを見つけるためにgetsizeof()を再帰的に使用する例については、再帰的なsizeofレシピを参照してください。すべての内容。」
juanpa.arrivillaga
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.