クラスの新しく作成されたインスタンスにリソースを割り当てるとき、python(cpythonだと思います)はどれくらい効率的ですか?ツリーを作成するためにノードクラスを何百万回もインスタンス化する必要がある状況があります構造。各ノードオブジェクトshould軽量で、いくつかの数値と親ノードと子ノードへの参照が含まれています。
たとえば、will pythonは、インスタンス化された各オブジェクトのすべての「二重アンダースコア」プロパティにメモリを割り当てる必要があります(たとえば、docstrings、__dict__
、__repr__
、__class__
など)、これらのプロパティを個別に作成するか、クラスで定義されている場所へのポインタを格納しますか?それとも、効率的で、各オブジェクトに保存する必要がある、定義したカスタムのもの以外は何も保存する必要がありませんか?
表面的には非常に単純です。メソッド、クラス変数、およびクラス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()
次に、メモリ内では次のようになります(非常に単純化されています)。
ただし、CPythonを深く掘り下げるときに重要なことがいくつかあります。
__weakref__
フィールド(別の8バイト)もあります。この時点で、CPythonがこれらの「問題」のいくつかを最適化することも指摘する必要があります。
__slots__
を使用して、__dict__
と__weakref__
を回避できます。これにより、インスタンスあたりのメモリフットプリントを大幅に減らすことができます。これらすべてと、これらのポイントのいくつか(特に最適化に関するポイント)が実装の詳細であることを考えると、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
ですが、しばらく使用していません。
[編集] pythonプロセス;ではない)でメモリ使用量を正確に測定するのは簡単ではありません私の答えは質問に完全に答えますが、それは場合によっては役立つかもしれない1つのアプローチです。
ほとんどのアプローチはプロキシメソッドを使用し(n個のオブジェクトを作成し、システムメモリへの影響を推定します)、外部ライブラリはそれらのメソッドをラップしようとします。たとえば、スレッドは here 、 here 、および 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クラス定義のサイズの測定値。 [/編集]
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__
を使用する各インスタンスは、属性値にのみスペースを使用します。
list
、dict
、Tuple
などのコンテナタイプから継承すると、属性(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よりも劣り、極端なフィールド数に対しても安定しています。
それは効率的で、各オブジェクトに保存する必要がある定義したカスタムのもの以外は何も保存する必要はありませんか?
いくつかの特定のスペースを除いて、ほぼはい。 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