web-dev-qa-db-ja.com

なぜタプルはリストよりもメモリ内のスペースが少ないのですか?

TupleはPythonでより少ないメモリ空間を使用します:

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

listsはより多くのメモリ空間を必要とします:

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

Pythonメモリ管理で内部的に何が起こりますか?

98
JON

私はあなたがCPythonと64ビットを使用していると仮定しています(CPython 2.7 64ビットでも同じ結果が得られました)。他のPython実装または32ビットPythonを使用している場合、違いが生じる可能性があります。

実装に関係なく、listsは可変サイズで、Tuplesは固定サイズです。

したがって、Tuplesは構造体内に要素を直接格納できますが、リストは間接化の層を必要とします(要素へのポインタを格納します)。この間接層は、64ビットである64ビットシステム上のポインターであり、したがって8バイトです。

しかし、listsが行うもう1つのことがあります。そうでない場合、list.appendO(n)操作alwaysになります-償却するためにO(1)(はるかに速い!!!)過剰に割り当てます。ただし、allocatedサイズとfilledを追跡する必要がありますサイズ(Tuplesは、1つのサイズのみを格納する必要があります。割り当てられたサイズと入力されたサイズは常に同一であるためです)。つまり、各リストには別の「サイズ」を格納する必要があり、これは64ビットシステムでは64ビット整数で、やはり8バイトです。

したがって、listsには、Tuplesより少なくとも16バイト多くのメモリが必要です。なぜ「少なくとも」と言ったのですか?過剰割り当てのため。過剰割り当てとは、必要以上のスペースを割り当てることを意味します。ただし、過剰割り当ての量は、リストの作成方法および追加/削除履歴によって異なります。

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

画像

上記の説明に付随するいくつかの画像を作成することにしました。たぶんこれらは役に立つ

これは、この例で(概略的に)メモリに保存される方法です。赤(フリーハンド)サイクルで違いを強調しました:

enter image description here

intオブジェクトもPythonオブジェクトであり、CPythonは小さな整数を再利用するため、これは実際の近似にすぎません。したがって、メモリ内のオブジェクトのより正確な表現(読みやすくはありませんが)は次のようになります。

enter image description here

便利なリンク:

__sizeof__は実際には「正しい」サイズを返さないことに注意してください!格納されている値のサイズのみを返します。ただし、 sys.getsizeof を使用すると、結果は異なります。

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

24の「余分な」バイトがあります。これらはrealです。これは、__sizeof__メソッドでは考慮されないガベージコレクタのオーバーヘッドです。これは、通常、マジックメソッドを直接使用することは想定されていないためです。この場合、それらの処理方法を知っている関数を使用します。 sys.getsizeof (実際には __sizeof__から返された値にGCオーバーヘッド を追加します。

130
MSeifert

サイズが実際に計算される方法を確認できるように、CPythonコードベースをさらに詳しく見ていきます。 あなたの特定の例では、過剰割り当ては実行されていないので、私はそれに触れません

ここで、64ビット値を使用します。


listsのサイズは、次の関数 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);
}

ここで Py_TYPE(self)selfob_typeを取得するマクロ(PyList_Typeを返す)であり、_PyObject_SIZEはそのタイプから tp_basicsize を取得する別のマクロです。 tp_basicsizesizeof(PyListObject)として計算されます。ここでPyListObjectはインスタンス構造体です。

PyListObject構造体 には3つのフィールドがあります。

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

これらには、それらが何であるかを説明するコメントがあります(私はこれをトリミングしました)。 PyObject_VAR_HEAD は、3つの8バイトフィールド(ob_refcountob_type、およびob_size)に展開されるため、24バイトの貢献となります。

したがって、今のところresは:

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

または:

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

リストインスタンスに割り当てられた要素がある場合。 2番目の部分では、貢献度を計算します。 self->allocatedは、その名前が示すとおり、割り当てられた要素の数を保持します。

要素がない場合、リストのサイズは次のように計算されます。

>>> [].__sizeof__()
40

つまり、インスタンス構造体のサイズ。


TupleオブジェクトはTuple_sizeof関数を定義しません。代わりに、 object_sizeof を使用してサイズを計算します。

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

これは、listsに関しては、tp_basicsizeを取得し、オブジェクトにゼロ以外のtp_itemsizeがある場合(可変長インスタンスがあることを意味する)、Tuple内のアイテムの数を乗算します( Py_SIZEを介して取得しますtp_itemsizeで。

tp_basicsizeは、再びsizeof(PyTupleObject)を使用します。ここで、 PyTupleObject構造体に含まれる

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

したがって、要素がない場合(つまり、Py_SIZE0を返します)、空のタプルのサイズはsizeof(PyTupleObject)と等しくなります。

>>> ().__sizeof__()
24

え?さて、ここに私が説明を見つけていない奇妙な点があります。Tuplesのtp_basicsizeは実際には次のように計算されます:

sizeof(PyTupleObject) - sizeof(PyObject *)

8から追加のtp_basicsizeバイトが削除された理由は、私が知ることができなかったものです。 (可能な説明についてはMSeifertのコメントを参照してください)


しかし、これは基本的にあなたの特定の例の違いですlistsは、割り当てられた要素の数も保持するため、いつ過剰割り当てするかを決定するのに役立ちます。

さて、追加の要素が追加されると、リストは実際にO(1)追加を達成するためにこの過剰割り当てを実行します。これにより、MSeifertの回答が適切にカバーされているため、サイズが大きくなります。

MSeifertの回答はそれを広くカバーしています。シンプルに保つために、次のことを考えることができます。

Tupleは不変です。一度設定すると、変更することはできません。そのため、そのオブジェクトに割り当てる必要があるメモリ量を事前に知っています。

listは可変です。アイテムを追加または削除できます。そのサイズを知る必要があります(内部実装用)。必要に応じてサイズを変更します。

無料の食事はありません-これらの機能には費用がかかります。したがって、リストのメモリのオーバーヘッド。

29
Chen A.

タプルのサイズには接頭辞が付いています。つまり、タプルの初期化時にインタープリターが含まれるデータに十分なスペースを割り当て、それが終わりであり、不変(変更不可)であるのに対して、リストは可変オブジェクトであるため動的ですメモリの割り当て。したがって、リストを追加または変更するたびにスペースを割り当てないように(変更されたデータを格納してデータをコピーするのに十分なスペースを割り当て)、将来の追加、変更などのために追加のスペースを割り当てます。それを合計。

3