web-dev-qa-db-ja.com

クラスのインスタンスはどのリソースを使用しますか?

クラスの新しく作成されたインスタンスにリソースを割り当てるとき、python(cpythonだと思います)はどれくらい効率的ですか?ツリーを作成するためにノードクラスを何百万回もインスタンス化する必要がある状況があります構造。各ノードオブジェクトshould軽量で、いくつかの数値と親ノードと子ノードへの参照が含まれています。

たとえば、will pythonは、インスタンス化された各オブジェクトのすべての「二重アンダースコア」プロパティにメモリを割り当てる必要があります(たとえば、docstrings、__dict____repr____class__など)、これらのプロパティを個別に作成するか、クラスで定義されている場所へのポインタを格納しますか?それとも、効率的で、各オブジェクトに保存する必要がある、定義したカスタムのもの以外は何も保存する必要がありませんか?

14
Alex

表面的には非常に単純です。メソッド、クラス変数、およびクラスdocstringはクラスに格納されます(関数docstringは関数に格納されます)。インスタンス変数はインスタンスに格納されます。インスタンスはクラスも参照するため、メソッドを検索できます。通常、それらはすべて辞書(__dict__)に保存されます。

そうです、簡単な答えは次のとおりです。Pythonはインスタンスにメソッドを格納しませんが、すべてのインスタンスにクラスへの参照が必要です。

たとえば、次のような単純なクラスがある場合:

class MyClass:
    def __init__(self):
        self.a = 1
        self.b = 2

    def __repr__(self):
        return f"{self.__class__.__name__}({self.a}, {self.b})"

instance_1 = MyClass()
instance_2 = MyClass()

次に、メモリ内では次のようになります(非常に単純化されています)。

enter image description here

深くなる

ただし、CPythonを深く掘り下げるときに重要なことがいくつかあります。

  • 抽象化としてディクショナリを使用すると、かなりのオーバーヘッドが発生します。インスタンスディクショナリ(バイト)への参照が必要であり、ディクショナリの各エントリには、ハッシュ(8バイト)、キーへのポインタ(8バイト)、および保存された属性(別の8バイト)。また、辞書は通常、別の属性を追加しても辞書のサイズ変更がトリガーされないように、過剰に割り当てられます。
  • Pythonには「値型」がなく、整数でもインスタンスになります。つまり、整数を格納するのに4バイトは必要ありません-Python(私のコンピューターでは)整数0を格納するのに24バイト、ゼロ以外の整数を格納するのに少なくとも28バイトが必要です。ただし他のオブジェクトへの参照には、8バイト(ポインター)が必要です。
  • CPythonは参照カウントを使用するため、各インスタンスには参照カウント(8バイト)が必要です。また、ほとんどのCPythonクラスは循環ガベージコレクターに参加します。これにより、インスタンスごとにさらに24バイトのオーバーヘッドが発生します。弱い参照が可能なこれらのクラス(ほとんどのクラス)に加えて、__weakref__フィールド(別の8バイト)もあります。

この時点で、CPythonがこれらの「問題」のいくつかを最適化することも指摘する必要があります。

  • Pythonは Key-Sharing Dictionaries を使用して、インスタンス辞書のメモリオーバーヘッド(ハッシュとキー)の一部を回避します。
  • クラスで__slots__を使用して、__dict____weakref__を回避できます。これにより、インスタンスあたりのメモリフットプリントを大幅に減らすことができます。
  • Pythonはいくつかの値をインターンします。たとえば、小さな整数を作成した場合、新しい整数インスタンスは作成されませんが、既存のインスタンスへの参照が返されます。

これらすべてと、これらのポイントのいくつか(特に最適化に関するポイント)が実装の詳細であることを考えると、Pythonクラスの有効なメモリ要件について標準的な答えを出すのは難しいです。

インスタンスのメモリフットプリントの削減

ただし、インスタンスのメモリフットプリントを削減したい場合は、必ず__slots__を試してください。それらには欠点がありますが、それらがあなたに当てはまらない場合、それらはメモリを減らすための非常に良い方法です。

class Slotted:
    __slots__ = ('a', 'b')
    def __init__(self):
        self.a = 1
        self.b = 1

それだけでは不十分で、多くの「値型」を操作する場合は、さらに一歩進んで拡張クラスを作成することもできます。これらはCで定義されているクラスですが、Pythonで使用できるようにラップされています。

便宜上、ここではCythonのIPythonバインディングを使用して、拡張クラスをシミュレートしています。

%load_ext cython
%%cython

cdef class Extensioned:
    cdef long long a
    cdef long long b

    def __init__(self):
        self.a = 1
        self.b = 1

メモリ使用量の測定

この理論の後に残っている興味深い質問は、どのようにして記憶を測定できるかということです。

私も通常のクラスを使用します:

class Dicted:
    def __init__(self):
        self.a = 1
        self.b = 1

私は通常、メモリへの影響を測定するために psutil (プロキシ方式ですが)を使用し、前後に使用したメモリの量を測定します。何らかの方法でインスタンスをメモリに保持する必要があるため、測定値は少しオフセットされています。そうしないと、メモリが(すぐに)再利用されます。また、これは概算にすぎません。Pythonは、特に作成/削除が多い場合に、実際にはかなりのメモリハウスキーピングを実行するためです。


import os
import psutil
process = psutil.Process(os.getpid())

runs = 10
instances = 100_000

memory_dicted = [0] * runs
memory_slotted = [0] * runs
memory_extensioned = [0] * runs

for run_index in range(runs):
    for store, cls in [(memory_dicted, Dicted), (memory_slotted, Slotted), (memory_extensioned, Extensioned)]:
        before = process.memory_info().rss
        l = [cls() for _ in range(instances)]
        store[run_index] = process.memory_info().rss - before
        l.clear()  # reclaim memory for instances immediately

Pythonは一部のメモリを再利用し、他の目的のためにメモリを保持することもあるため、実行ごとにメモリが完全に同一になるわけではありませんが、少なくとも妥当なヒントが得られるはずです。

>>> min(memory_dicted) / 1024**2, min(memory_slotted) / 1024**2, min(memory_extensioned) / 1024**2
(15.625, 5.3359375, 2.7265625)

ここでminを使用したのは、主に最小値が何であるかに興味があり、バイトをメガバイトに変換するために1024**2で割ったためです。

概要:予想どおり、dictを使用する通常のクラスは、スロットを使用するクラスよりも多くのメモリを必要としますが、拡張クラス(該当する場合)のメモリフットプリントはさらに低くなる可能性があります。

メモリ使用量を測定するのに非常に便利なもう1つのツールは、 memory_profiler ですが、しばらく使用していません。

12
MSeifert

[編集] pythonプロセス;ではない)でメモリ使用量を正確に測定するのは簡単ではありません私の答えは質問に完全に答えますが、それは場合によっては役立つかもしれない1つのアプローチです。

ほとんどのアプローチはプロキシメソッドを使用し(n個のオブジェクトを作成し、システムメモリへの影響を推定します)、外部ライブラリはそれらのメソッドをラップしようとします。たとえば、スレッドは herehere 、および there [/ edit]にあります。

オン cPython 3.7、通常のクラスインスタンスの最小サイズは56バイトです。 __slots__(辞書なし)、16バイト。

import sys

class A:
    pass

class B:
    __slots__ = ()
    pass

a = A()
b = B()
sys.getsizeof(a), sys.getsizeof(b)

出力:

56, 16

Docstring、クラス変数、および型注釈がインスタンスレベルで見つかりません。

import sys

class A:
    """regular class"""
    a: int = 12

class B:
    """slotted class"""
    b: int = 12
    __slots__ = ()

a = A()
b = B()
sys.getsizeof(a), sys.getsizeof(b)

出力:

56, 16

[編集]さらに、 @ LiuXiMin answerクラス定義のサイズの測定値。 [/編集]

9

CPythonの最も基本的なオブジェクトは、 タイプ参照と参照数 です。どちらもワードサイズ(つまり、64ビットマシンでは8バイト)であるため、インスタンスの最小サイズは2ワード(つまり、64ビットマシンでは16バイト)です。

>>> import sys
>>>
>>> class Minimal:
...      __slots__ = ()  # do not allow dynamic fields
...
>>> minimal = Minimal()
>>> sys.getsizeof(minimal)
16

各インスタンスには、__class__用のスペースと非表示の参照カウントが必要です。


タイプ参照(おおよそobject.__class__)は、インスタンスがクラスからコンテンツをフェッチするを意味します。インスタンスではなく、クラスで定義するすべてのものが、インスタンスごとにスペースを占有することはありません。

>>> class EmptyInstance:
...      __slots__ = ()  # do not allow dynamic fields
...      foo = 'bar'
...      def hello(self):
...          return "Hello World"
...
>>> empty_instance = EmptyInstance()
>>> sys.getsizeof(empty_instance)  # instance size is unchanged
16
>>> empty_instance.foo             # instance has access to class attributes
'bar'
>>> empty_instance.hello()         # methods are class attributes!
'Hello World'

メソッドもクラスの関数であることに注意してください。インスタンスを介してフェッチすると、 関数のデータ記述子プロトコル が呼び出され、インスタンスを関数に部分的にバインドして一時的なメソッドオブジェクトが作成されます。その結果、メソッドはインスタンスサイズを増加させません

インスタンスは、__doc__およびanyメソッドを含むクラス属性用のスペースを必要としません。


インスタンスのサイズを大きくする唯一のものは、インスタンスに保存されているコンテンツです。これを実現するには、__dict____slots__、および コンテナタイプ の3つの方法があります。これらはすべて、何らかの方法でインスタンスに割り当てられたコンテンツを保存します。

  • デフォルトでは、インスタンスには __dict__フィールド -属性を格納するマッピングへの参照があります。このようなクラスまたには、__weakref__のような他のデフォルトフィールドがいくつかあります。

    >>> class Dict:
    ...     # class scope
    ...     def __init__(self):
    ...         # instance scope - access via self
    ...         self.bar = 2                   # assign to instance
    ...
    >>> dict_instance = Dict()
    >>> dict_instance.foo = 1                  # assign to instance
    >>> sys.getsizeof(dict_instance)           # larger due to more references
    56
    >>> sys.getsizeof(dict_instance.__dict__)  # __dict__ takes up space as well!
    240
    >>> dict_instance.__dict__                 # __dict__ stores attribute names and values
    {'bar': 2, 'foo': 1}
    

    __dict__を使用する各インスタンスは、dict、属性名、および値にスペースを使用します。

  • __slots__フィールドをクラスに追加 は、固定データレイアウトのインスタンスを生成します。これにより、許可される属性が宣言された属性に制限されますが、インスタンス上でほとんどスペースを占有しません。 __dict__および__weakref__スロットは、要求があった場合にのみ作成されます。

    >>> class Slots:
    ...     __slots__ = ('foo',)  # request accessors for instance data
    ...     def __init__(self):
    ...         # instance scope - access via self
    ...         self.foo = 2
    ...
    >>> slots_instance = Slots()
    >>> sys.getsizeof(slots_instance)           # 40 + 8 * fields
    48
    >>> slots_instance.bar = 1
    AttributeError: 'Slots' object has no attribute 'bar'
    >>> del slots_instance.foo
    >>> sys.getsizeof(slots_instance)           # size is fixed
    48
    >>> Slots.foo                               # attribute interface is descriptor on class
    <member 'foo' of 'Slots' objects>
    

    __slots__を使用する各インスタンスは、属性値にのみスペースを使用します。

  • listdictTupleなどのコンテナタイプから継承すると、属性(self[0])の代わりにアイテム(self.a)を格納できます。 。これは、コンパクトな内部ストレージさらに__dict__または__slots__のいずれかに使用します。このようなクラスが手動で作成されることはめったにありません。typing.NamedTupleなどのヘルパーがよく使用されます。

    >>> from typing import NamedTuple
    >>>
    >>> class Named(NamedTuple):
    ...     foo: int
    ...
    >>> named_instance = Named(2)
    >>> sys.getsizeof(named_instance)
    56
    >>> named_instance.bar = 1
    AttributeError: 'Named' object has no attribute 'bar'
    >>> del named_instance.foo                  # behaviour inherited from container
    AttributeError: can't delete attribute
    >>> Named.foo                               # attribute interface is descriptor on class
    <property at 0x10bba3228>
    >>> Named.__len__                           # container interface/metadata such as length exists
    <slot wrapper '__len__' of 'Tuple' objects>
    

    派生コンテナの各インスタンスは、基本タイプのように動作し、さらに潜在的な__slots__または__dict__

最も軽量なインスタンスは__slots__を使用して、属性値のみを格納します。


__dict__オーバーヘッドの一部は、通常Pythonインタープリターによって最適化されます。CPythonは インスタンス間でキーを共有 することができます インスタンスあたりのサイズを大幅に削減 。PyPyは、__dict____slots__の間の 完全に違いを排除する 最適化されたキー共有表現を使用します。

最も些細な場合を除いて、オブジェクトのメモリ消費量を正確に測定することはできません。分離されたオブジェクトのサイズを測定すると、__dict__のメモリを使用して両方インスタンス上のポインタおよび外部のdictなどの関連する構造が失われます。オブジェクトのグループを測定すると、共有オブジェクト(インターン文字列、小さな整数など)と遅延オブジェクト(たとえば、__dict__dictはアクセスされた場合にのみ存在します)のカウントが誤ってしまいます。 PyPy sys.getsizeof誤用を避けるため を実装していないことに注意してください。

メモリ消費量を測定するには、完全なプログラム測定を使用する必要があります。たとえば、 resource または psutilsを使用して、オブジェクトの生成中に独自のメモリ消費量を取得できます

そのようなものを1つ作成しました フィールド数インスタンス数および実装バリアント の測定スクリプト。表示される値は、CPython3.7.0およびPyPy33.6.1/7.1.1-beta0で、インスタンス数が1000000の場合のバイト/フィールドです。

      # fields |     1 |     4 |     8 |    16 |    32 |    64 |
---------------+-------+-------+-------+-------+-------+-------+
python3: slots |  48.8 |  18.3 |  13.5 |  10.7 |   9.8 |   8.8 |
python3: dict  | 170.6 |  42.7 |  26.5 |  18.8 |  14.7 |  13.0 |
pypy3:   slots |  79.0 |  31.8 |  30.1 |  25.9 |  25.6 |  24.1 |
pypy3:   dict  |  79.2 |  31.9 |  29.9 |  27.2 |  24.9 |  25.0 |

CPythonの場合、__slots____dict__と比較してメモリを約30%〜50%節約します。 PyPyの場合、消費量は同等です。興味深いことに、PyPyは__slots__を使用したCPythonよりも劣り、極端なフィールド数に対しても安定しています。

7
MisterMiyagi

それは効率的で、各オブジェクトに保存する必要がある定義したカスタムのもの以外は何も保存する必要はありませんか?

いくつかの特定のスペースを除いて、ほぼはい。 Pythonはすでにメタクラスと呼ばれるtypeのインスタンスです。クラスオブジェクトの新しいインスタンスの場合、custom stuff__init__のクラスです。で定義されている属性とメソッドはクラスはそれ以上のスペースを費やしません。

いくつかの特定のスペースについては、ReblochonMasqueの答えを参照してください。非常に優れていて印象的です。

たぶん私は1つの簡単だが説明的な例を与えることができます:

class T(object):
    def a(self):
        print(self)
t = T()
t.a()
# output: <__main__.T object at 0x1060712e8>
T.a(t)
# output: <__main__.T object at 0x1060712e8>
# as you see, t.a() equals T.a(t)

import sys
sys.getsizeof(T)
# output: 1056
sys.getsizeof(T())
# output: 56
5
LiuXiMin