Ale*_*lex 14 python memory-management cpython class instance
How efficient is python (cpython I guess) when allocating resources for a newly created instance of a class? I have a situation where I will need to instantiate a node class millions of times to make a tree structure. Each of the node objects should be lightweight, just containing a few numbers and references to parent and child nodes.
For example, will python need to allocate memory for all the "double underscore" properties of each instantiated object (e.g. the docstrings, __dict__, __repr__, __class__, etc, etc), either to create these properties individually or store pointers to where they are defined by the class? Or is it efficient and does not need to store anything except the custom stuff I defined that needs to be stored in each object?
MSe*_*ert 12
从表面上看,这很简单:方法,类变量和类docstring存储在类中(函数docstring存储在函数中)。实例变量存储在实例中。该实例还引用该类,因此您可以查找方法。通常,它们全部存储在字典(the __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()
Run Code Online (Sandbox Code Playgroud)
然后在内存中看起来(非常简化)如下:
但是,深入了解CPython时,有一些重要的事情:
__weakref__字段(另外8个字节)。在这一点上,也有必要指出,CPython针对其中的一些“问题”进行了优化:
__slots__在类中使用来避免__dict__和__weakref__。这样可以大大减少每个实例的内存占用。鉴于所有这些,而且其中的某些要点(尤其是有关优化的要点)都是实现细节,因此很难给出关于Python类有效内存需求的规范答案。
但是,如果您想减少实例的内存占用,一定__slots__要尝试一下。它们确实有缺点,但万一它们不适用于您,这是减少内存的一种很好的方法。
class Slotted:
__slots__ = ('a', 'b')
def __init__(self):
self.a = 1
self.b = 1
Run Code Online (Sandbox Code Playgroud)
如果这还不够,并且您要使用许多“值类型”,那么您还可以更进一步,创建扩展类。这些是用C定义但包装的类,以便您可以在Python中使用它们。
为了方便起见,我在这里使用Cython的IPython绑定来模拟扩展类:
%load_ext cython
Run Code Online (Sandbox Code Playgroud)
%%cython
cdef class Extensioned:
cdef long long a
cdef long long b
def __init__(self):
self.a = 1
self.b = 1
Run Code Online (Sandbox Code Playgroud)
在所有这些理论之后,剩下的有趣的问题是:我们如何测量内存?
我也使用普通的类:
class Dicted:
def __init__(self):
self.a = 1
self.b = 1
Run Code Online (Sandbox Code Playgroud)
我通常使用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
Run Code Online (Sandbox Code Playgroud)
每次运行的内存不会完全相同,因为Python重用了一些内存,有时还会为其他目的保留内存,但是它至少应该给出一个合理的提示:
>>> min(memory_dicted) / 1024**2, min(memory_slotted) / 1024**2, min(memory_extensioned) / 1024**2
(15.625, 5.3359375, 2.7265625)
Run Code Online (Sandbox Code Playgroud)
我min之所以在这里使用它,主要是因为我对最小最小值感兴趣,然后除以1024**2将字节转换为兆字节。
简介:正如预期的那样,带有dict的普通类将比带有插槽的类需要更多的内存,但是扩展类(如果适用和可用)的内存占用量甚至更低。
另一个可以方便地测量内存使用情况的工具是memory_profiler,尽管我已经有一段时间没有使用它了。
[edit]通过python进程获得内存使用情况的准确度量并不容易;我认为我的回答不能完全回答问题,但这是一种在某些情况下可能有用的方法。
大多数方法使用代理方法(创建n个对象并估计对系统内存的影响),而外部库则尝试包装这些方法。例如,可以在这里,这里和那里找到线程[/ 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)
Run Code Online (Sandbox Code Playgroud)
56, 16
Run Code Online (Sandbox Code Playgroud)
在实例级别找不到文档字符串,类变量和类型注释:
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)
Run Code Online (Sandbox Code Playgroud)
56, 16
Run Code Online (Sandbox Code Playgroud)
[edit]此外,请参见@LiuXiMin答案以了解类定义的大小。[/编辑]
CPython中最基本的对象只是类型引用和引用计数。两者都是字大小的(即在64位计算机上为8字节),因此实例的最小大小为2个字(即在64位计算机上为16字节)。
>>> import sys
>>>
>>> class Minimal:
... __slots__ = () # do not allow dynamic fields
...
>>> minimal = Minimal()
>>> sys.getsizeof(minimal)
16
Run Code Online (Sandbox Code Playgroud)
每个实例都需要空间__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'
Run Code Online (Sandbox Code Playgroud)
注意,方法也是类上的函数。通过实例获取一个实例将调用该函数的数据描述符协议,以通过将实例部分绑定到该函数来创建一个临时方法对象。结果,方法不会增加实例大小。
实例不需要空间来容纳类属性,包括__doc__和任何方法。
唯一增加实例大小的是存储在实例上的内容。有三种方法来实现这一目标:__dict__,__slots__和容器类型。所有这些存储内容都以某种方式分配给实例。
默认情况下,实例具有一个__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}
Run Code Online (Sandbox Code Playgroud)
每个使用的实例都会__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>
Run Code Online (Sandbox Code Playgroud)
使用的每个实例__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>
Run Code Online (Sandbox Code Playgroud)
派生容器的每个实例的行为都类似于基本类型,再加上电位__slots__或__dict__。
最轻量的实例__slots__仅用于存储属性值。
注意,一部分__dict__开销通常是由Python解释器优化的。CPython能够在实例之间共享密钥,这可以大大减少每个实例的大小。PyPy使用了优化的键共享表示形式,从而完全消除了__dict__和之间的区别__slots__。
除了最琐碎的情况之外,不可能精确地测量对象的内存消耗。测量分离对象的大小未命中有关的结构,例如__dict__使用存储器两者上的实例的指针和一个外部dict。测量对象组会误计数共享对象(中间字符串,小整数等)和惰性对象(例如dict,__dict__只有在访问时存在)。请注意,PyPy 并没有实施sys.getsizeof 以避免其滥用。
为了测量内存消耗,应使用完整的程序测量值。例如,在生成对象时,可以使用resource或psutils获取自己的内存消耗。
我创建了一个这样的测量脚本场的数量,实例数和实施变。在CPython 3.7.0和PyPy3 3.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 |
Run Code Online (Sandbox Code Playgroud)
对于CPython,__slots__与相比,可以节省大约30%-50%的内存__dict__。对于PyPy,消耗量是可比的。有趣的是,PyPy的CyPy比CPython差__slots__,并且对于极端字段计数保持稳定。
它是否有效,除了我定义的需要存储在每个对象中的自定义内容外,不需要存储任何内容?
几乎可以,除了某些空间。Python中的类已经是的实例type,称为元类。当新添加类对象的实例时,中的custom stuff就是这些__init__。类中定义的属性和方法不会花费更多空间。
至于某些空间,请参考Reblochon Masque的回答,非常好且令人印象深刻。
也许我可以举一个简单但说明性的例子:
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
Run Code Online (Sandbox Code Playgroud)
| 归档时间: |
|
| 查看次数: |
500 次 |
| 最近记录: |