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


105

A tupleはPythonで必要なメモリ領域が少なくなります。

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

一方、listsはより多くのメモリ領域を必要とします。

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

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


1
これが内部でどのように機能するかはわかりませんが、リストオブジェクトには少なくともタプルにはない追加などの関数が少なくともあります。したがって、タプルがより単純なタイプのオブジェクトであるほど小さくなります
Metareven

私がチェックしたときに、私は(1,2,3)[1,2,3] = 72 aとbを取る=それはまた、私のために....マシンにマシンに依存だと思うと、88を取る
Amrit

6
Pythonタプルは不変です。可変オブジェクトには、実行時の変更に対処するための追加のオーバーヘッドがあります。
リーダニエルクロッカー2017年

@Metareven型が持つメソッドの数は、インスタンスが使用するメモリ空間に影響を与えません。メソッドリストとそのコードはオブジェクトプロトタイプによって処理されますが、インスタンスはデータと内部変数のみを格納します。
jjmontes 2017年

回答:


144

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

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

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

ただし、別のことをlist行う:それらは過剰に割り当てます。それ以外の場合list.append常にO(n)操作になります -償却するためO(1)(はるかに高速!!!)、割り当て超過です。しかし、割り当てられたサイズと塗りつぶされたサイズを追跡tupleする必要があります(割り当てられたサイズと塗りつぶされたサイズは常に同一であるため、sは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

画像

上記の説明に合わせていくつかの画像を作成することにしました。多分これらは役に立ちます

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

ここに画像の説明を入力してください

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

ここに画像の説明を入力してください

役立つリンク:

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

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

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


リストはタプルよりも少なくとも16バイト多くのメモリが必要です。」、それは8ではないでしょうか?タプルの1つのサイズとリストの2つのサイズは、リストの1つの追加サイズを意味します。
池上

1
はい、リストには1つの追加の「サイズ」(8バイト)がありますが、構造体に直接格納するのではなく(タプルが行うこと)、「PyObjectの配列」へのポインター(8バイト)も格納します。8 + 8 = 16。
MSeifert 2017年

2
もう一つの便利に関するリンクlistメモリアロケーションstackoverflow.com/questions/40018398/...
vishes_shell

@vishes_shell質問のコードは過剰に割り当てられないため、これは実際には質問とは関係ありません。しかし、はい、使用時の過剰割り当ての量list()やリスト内包について詳しく知りたい場合に役立ちます。
MSeifert 2017年

1
@ user3349993タプルは不変なので、タプルに追加したり、タプルからアイテムを削除したりすることはできません。
MSeifert 2017年

31

サイズが実際に計算される方法を確認できるように、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)グラブマクロであるob_typeのは、self(復帰PyList_Type)、一方で _PyObject_SIZE別のマクロであるグラブtp_basicsizeその型から。where はインスタンス構造体tp_basicsizeとして計算されます。sizeof(PyListObject)PyListObject

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

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

これらはそれらが何であるかを説明するコメント(私はトリミングしました)を持っています、それらを読むために上のリンクをたどってください。PyObject_VAR_HEAD3つの8バイトフィールド(ob_refcountob_typeおよびob_size)に拡張されるため、1 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(可変長のインスタンスを持つことを意味します)を取得し、(を介して取得するPy_SIZE)タプル内の項目数をで乗算しtp_itemsizeます。

tp_basicsize構造体が含まれているsizeof(PyTupleObject)場所を 再び使用しPyTupleObjectます

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

したがって、要素がない(つまりをPy_SIZE返す0)と、空のタプルのサイズは次のようになりますsizeof(PyTupleObject)

>>> ().__sizeof__()
24

え?さて、ここで私は、の説明を発見していない風変わりだtp_basicsizetuple、以下のようにsは実際に計算されますが。

sizeof(PyTupleObject) - sizeof(PyObject *)

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


しかし、これは基本的にあなたの具体的な例の違いです。listsはまた、割り当てられた要素の数を維持します。

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


ob_item[1]はほとんどがプレースホルダーであると信じています(そのため、basicsizeから差し引かれているのは理にかなっています)。tuple使用して割り当てられていますPyObject_NewVar。私は詳細を理解していませんので、それは単なる教育的な推測です...
MSeifert

@MSeifert申し訳ありませんが、修正されました:-)。本当にわからない。ある時点で過去に見つけたのを覚えているが、あまり注意を払っていない。多分私は将来のある時点で質問するだけだろう:-)
Dimitris Fasarakis Hilliard

29

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

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

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

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


3

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

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.