单看数值的话,pympler 似乎确实比 getsizeof 合理多了。
再看看 pysize,直接看测试结果是(获取其源码过程略):
64
118
190
206
300281
30281
可以看出,它比 pympler 计算的结果略小。就两个项目的完整度、使用量与社区贡献者规模来看,pympler 的结果似乎更为可信。
那么,它们分别是怎么实现的呢?那微小的差异是怎么导致的?从它们的实现方案中,我们可以学习到什么呢?
pysize 项目很简单,只有一个核心方法:
def get_size(obj, seen=None):
"""Recursively finds size of objects in bytes"""
size = sys.getsizeof(obj)
if seen is None:
seen = set
obj_id = id(obj)
if obj_id in seen:
return 0
# Important mark as seen *before* entering recursion to gracefully handle
# self-referential objects
seen.add(obj_id)
if hasattr(obj, '__dict__'):
for cls in obj.__class__.__mro__:
if '__dict__' in cls.__dict__:
d = cls.__dict__['__dict__']
if inspect.isgetsetdescriptor(d) or inspect.ismemberdescriptor(d):
size = get_size(obj.__dict__, seen)
break
if isinstance(obj, dict):
size = sum((get_size(v, seen) for v in obj.values))
size = sum((get_size(k, seen) for k in obj.keys))
elif hasattr(obj, '__iter__') and not isinstance(obj, (str, bytes, bytearray)):
size = sum((get_size(i, seen) for i in obj))
if hasattr(obj, '__slots__'): # can have __slots__ with __dict__
size = sum(get_size(getattr(obj, s), seen) for s in obj.__slots__ if hasattr(obj, s))
return size
除去判断__dict__
和__slots__
属性的部分(针对类对象),它主要是对字典类型及可迭代对象(除字符串、bytes、bytearray)作递归的计算,逻辑并不复杂。
以 [1,2] 这个列表为例,它先用 sys.getsizeof 算出 36 字节,再计算内部的两个元素得 14*2=28 字节,最后相加得到 64 字节。
相比之下,pympler 所考虑的内容要多很多,入口在这:
def asizeof(self, *objs, **opts):
'''Return the combined size of the given objects
(with modified options, see method **set**).
'''
if opts:
self.set(**opts)
self.exclude_refs(*objs) # skip refs to objs
return sum(self._sizer(o, 0, 0, None) for o in objs)
它可以接受多个参数,再用 sum 方法合并。所以核心的计算方法其实是 _sizer。但代码很复杂,绕来绕去像一座迷宫:
def _sizer(self, obj, pid, deep, sized): # MCCABE 19
'''Size an object, recursively.
'''
s, f, i = 0, 0, id(obj)
if i not in self._seen:
self._seen[i] = 1
elif deep or self._seen[i]:
# skip obj if seen before
# or if ref of a given obj
self._seen.again(i)
if sized:
s = sized(s, f, name=self._nameof(obj))
self.exclude_objs(s)
return s # zero
else: # deep == seen[i] == 0
self._seen.again(i)
try:
k, rs = _objkey(obj),
if k in self._excl_d:
self._excl_d[k] = 1
else:
v = _typedefs.get(k, None)
if not v: # new typedef
_typedefs[k] = v = _typedef(obj, derive=self._derive_,
frames=self._frames_,
infer=self._infer_)
if (v.both or self._code_) and v.kind is not self._ign_d:
# 猫注:这里计算 flat size
s = f = v.flat(obj, self._mask) # flat size
if self._profile:
# profile based on *flat* size
self._prof(k).update(obj, s)
# recurse, but not for nested modules
if v.refs and deep < self._limit_ \
and not (deep and ismodule(obj)):
# add sizes of referents
z, d = self._sizer, deep 1
if sized and deep < self._detail_:
# use named referents
self.exclude_objs(rs)
for o in v.refs(obj, True):
if isinstance(o, _NamedRef):
r = z(o.ref, i, d, sized)
r.name = o.name
else:
r = z(o, i, d, sized)
r.name = self._nameof(o)
rs.append(r)
s = r.size
else: # just size and accumulate
for o in v.refs(obj, False):
# 猫注:这里递归计算 item size
s = z(o, i, d, None)
# deepest recursion reached
if self._depth < d:
self._depth = d
if self._stats_ and s > self._above_ > 0:
# rank based on *total* size
self._rank(k, obj, s, deep, pid)
except RuntimeError: # XXX RecursionLimitExceeded:
self._missed = 1
if not deep:
self._total = s # accumulate
if sized:
s = sized(s, f, name=self._nameof(obj), refs=rs)
self.exclude_objs(s)
return s
它的核心逻辑是把每个对象的 size 分为两部分:flat size 和 item size。
计算 flat size 的逻辑在:
def flat(self, obj, mask=0):
'''Return the aligned flat size.
'''
s = self.base
if self.leng and self.item > 0: # include items
s = self.leng(obj) * self.item
# workaround sys.getsizeof (and numpy?) bug ... some
# types are incorrectly sized in some Python versions
# (note, isinstance(obj, ) == False)
# 猫注:不可 sys.getsizeof 的,则用上面逻辑,可以的,则用下面逻辑
if not isinstance(obj, _getsizeof_excls):
s = _getsizeof(obj, s)
if mask: # align
s = (s mask) & ~mask
return s
这里出现的 mask 是为了作字节对齐,默认值是 7,该计算公式表示按 8 个字节对齐。对于 [1,2] 列表,会算出 (36 7)&~7=40 字节。同理,对于单个的 item,比如列表中的数字 1,sys.getsizeof(1) 等于 14,而 pympler 会算成对齐的数值 16,所以汇总起来是 40 16 16=72 字节。这就解释了为什么 pympler 算的结果比 pysize 大。
字节对齐一般由具体的编译器实现,而且不同的编译器还会有不同的策略,理论上 Python 不应关心这么底层的细节,内置的 getsizeof 方法就没有考虑字节对齐。
在不考虑其它 edge cases 的情况下,可以认为 pympler 是在 getsizeof 的基础上,既考虑了遍历取引用对象的 size,又考虑到了实际存储时的字节对齐问题,所以它会显得更加贴近现实。
小结getsizeof 方法的问题是显而易见的,我创造了一个“浅计算”概念给它。这个概念借鉴自 copy 方法的“浅拷贝”,同时对应于 deepcopy “深拷贝”,我们还能推理出一个“深计算”。
前面展示了两个试图实现“深计算”的项目(pysize pympler),两者在浅计算的基础上,深入地求解引用对象的大小。pympler 项目的完整度较高,代码中有很多细节上的设计,比如字节对齐。
Python 官方团队当然也知道 getsizeof 方法的局限性,他们甚至在文档中加了一个链接 [3],指向了一份实现深计算的示例代码。那份代码比 pysize 还要简单(没有考虑类对象的情况)。
未来 Python 中是否会出现深计算的方法,假设命名为 getdeepsizeof 呢?这不得而知了。
本文的目的是加深对 getsizeof 方法的理解,区分浅计算与深计算,分析两个深计算项目的实现思路,指出几个值得注意的问题。
Python 内存分配时的小秘密:https://dwz.cn/AoSdCZfo
Python中对象的内存使用(一):https://dwz.cn/SXGtXklz
[1] https://dwz.cn/yxg72lyS
[2] https://dwz.cn/5m83JStN
[3] https://code.activestate.com/recipes/577504
作者简介:豌豆花下猫,生于广东毕业于武大,现为苏漂程序员,有一些极客思维,也有一些人文情怀,有一些温度,还有一些态度。
【end】
在这次疫情防控中,无感人体测温系统发挥了怎样的作用?它的技术原理是什么?无感人体测温系统的应用场景中有哪些关键技术与落地困难?高精准的无感人体测温系统的核心技术武器是什么?对于开发者们来说,大家应该了解哪些技术?
机器会成为神吗?
6个步骤,告诉你如何用树莓派和机器学习DIY一个车牌识别器!(附详细分析)
微信回应钉钉健康码无法访问;谷歌取消年度I/O开发者大会;微软公布Visual Studio最新路线图
什么是CD管道?一文告诉你如何借助Kubernetes、Ansible和Jenkins创建CD管道!
智能合约初探:概念与演变
血亏1.5亿元!微盟耗时145个小时弥补删库