类的实例使用什么资源?

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时,有一些重要的事情:

  • 使用字典作为抽象会带来很多开销:您需要引用实例字典(字节),并且字典中的每个条目都存储哈希(8字节),指向键的指针(8字节)和指向指针的指针。存储的属性(另外8个字节)。另外,字典通常会过度分配,因此添加另一个属性不会触发字典调整大小。
  • Python没有“值类型”,即使是整数也将是一个实例。这意味着您不需要4个字节来存储整数-Python(在我的计算机上)需要24个字节来存储整数0,至少需要28个字节来存储非零的整数。但是,对其他对象的引用仅需要8个字节(指针)。
  • CPython使用引用计数,因此每个实例都需要一个引用计数(8字节)。同样,大多数CPython类都参与了循环垃圾收集器,这会导致每个实例增加24字节的开销。除了这些可以弱引用的类(大多数)之外,还有一个__weakref__字段(另外8个字节)。

在这一点上,也有必要指出,CPython针对其中的一些“问题”进行了优化:

  • Python使用“ 密钥共享字典”来避免实例字典的某些内存开销(哈希和密钥)。
  • 您可以__slots__在类中使用来避免__dict____weakref__。这样可以大大减少每个实例的内存占用。
  • Python会实习一些值,例如,如果您创建一个小整数,它将不会创建新的整数实例,而是返回对现有实例的引用。

鉴于所有这些,而且其中的某些要点(尤其是有关优化的要点)都是实现细节,因此很难给出关于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,尽管我已经有一段时间没有使用它了。


Reb*_*que 9

[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答案了解类定义的大小。[/编辑]


Mis*_*agi 7

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__仅将空间用于属性值。

  • 从容器类型继承,如listdict或者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 以避免其滥用

为了测量内存消耗,应使用完整的程序测量值。例如,在生成对象时,可以使用resourcepsutils获取自己的内存消耗

我创建了一个这样的测量脚本场的数量实例数实施变。在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__,并且对于极端字段计数保持稳定。


Liu*_*Min 5

它是否有效,除了我定义的需要存储在每个对象中的自定义内容外,不需要存储任何内容?

几乎可以,除了某些空间。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 次

最近记录:

6 年,11 月 前