Python 和 speed/RAM 性能中的(命名)元组字典
Dictionary of (named) tuples in Python and speed/RAM performance
我正在创建一个字典 d
包含一百万个元组项,理想情况下我想通过以下方式访问它们:
d[1634].id # or d[1634]['id']
d[1634].name # or d[1634]['name']
d[1634].isvalid # or d[1634]['isvalid']
而不是 d[1634][0]
、d[1634][1]
、d[1634][2]
,后者不太明确。
根据我的测试:
import os, psutil, time, collections, typing
Tri = collections.namedtuple('Tri', 'id,name,isvalid')
Tri2 = typing.NamedTuple("Tri2", [('id', int), ('name', str), ('isvalid', bool)])
t0 = time.time()
# uncomment only one of these 4 next lines:
d = {i: (i+1, 'hello', True) for i in range(1000000)} # tuple
# d = {i: {'id': i+1, 'name': 'hello', 'isvalid': True} for i in range(1000000)} # dict
# d = {i: Tri(id=i+1, name='hello', isvalid=True) for i in range(1000000)} # namedtuple
# d = {i: Tri2(id=i+1, name='hello', isvalid=True) for i in range(1000000)} # NamedTuple
print('%.3f s %.1f MB' % (time.time()-t0, psutil.Process(os.getpid()).memory_info().rss / 1024 ** 2))
"""
tuple: 0.257 s 193.3 MB
dict: 0.329 s 363.6 MB
namedtuple: 1.253 s 193.3 MB (collections)
NamedTuple: 1.250 s 193.5 MB (typing)
"""
- 与
tuple
相比,使用 dict
可使 RAM 使用量翻倍
- 与
tuple
! 相比,使用 namedtuple
或 NamedTuple
花费的时间乘以 5
问题:Python 3 中是否有类似元组的数据结构,它允许使用 x.id
、x.name
等访问数据,以及RAM 和 CPU 有效率吗?
备注:
在我的实际用例中,tuple
类似于 (uint64, uint64, bool)
.
类型的 C 结构
我也试过:
slots
(避免内部对象的__dict__
,见Usage of __slots__?)
dataclass
:
@dataclasses.dataclass
class Tri3:
id: int
...
ctypes.Structure
:
class Tri7(ctypes.Structure):
_fields_ = [("id", ctypes.c_int), ...]
但它并没有更好(所有这些都是 ~ 1.2 秒),在性能方面与真正的 tuple
相去甚远
以下是其他选项:C-like structures in Python
您可以尝试反转它(存储为数组结构)并以 x['id'][1634]
的形式访问值。换句话说,x
是一个具有三个键的字典,每个键的值是一个列表。这将是 space 高效的。
或者您可以使用 pandas 数据帧。数据帧以矩阵形式存储,其中行具有数字 ID,列具有标签(如 'name' 等字符串)。对于数据框 df
,df.iloc[i]
指向第 $i^th$ 行,您可以通过 df.iloc[i].name
或 df.iloc[i]['name']
访问该行中的名称
Cython 的 cdef-classes 可能是您想要的:它们比纯 Python 类 使用更少的内存,即使它在访问成员时以更多的开销为代价(因为字段存储为 C 值而不是 Python-对象)。
例如:
%%cython
cdef class CTuple:
cdef public unsigned long long int id
cdef public str name
cdef public bint isvalid
def __init__(self, id, name, isvalid):
self.id = id
self.name = name
self.isvalid = isvalid
可以随意使用:
ob=CTuple(1,"mmm",3)
ob.id, ob.name, ob.isvalid # prints (2, "mmm", 3)
Timings/memory消费:
首先,我机器上的基线:
0.258 s 252.4 MB # tuples
0.343 s 417.5 MB # dict
1.181 s 264.0 MB # namedtuple collections
与 CTuple
我们得到:
0.306 s 191.0 MB
几乎一样快,需要的内存要少得多。
如果编译时成员的 C 类型不明确,可以使用简单的 python-objects:
%%cython
cdef class PTuple:
cdef public object id
cdef public object name
cdef public object isvalid
def __init__(self, id, name, isvalid):
self.id = id
self.name = name
self.isvalid = isvalid
时间安排有点令人惊讶:
0.648 s 249.8 MB
我没想到它会比 CTuple
版本慢那么多,但至少它比命名元组快两倍。
这种方法的一个缺点是它需要编译。然而,Cython 提供 cython.inline
可用于编译动态创建的 Cython 代码。
我已经发布了 cynamedtuple
,可以通过 pip install cynamedtuple
安装,它基于下面的原型:
import cython
# for generation of cython code:
tab = " "
def create_members_definition(name_to_ctype):
members = []
for my_name, my_ctype in name_to_ctype.items():
members.append(tab+"cdef public "+my_ctype+" "+my_name)
return members
def create_signature(names):
return tab + "def __init__(self,"+", ".join(names)+"):"
def create_initialization(names):
inits = [tab+tab+"self."+x+" = "+x for x in names]
return inits
def create_cdef_class_code(classname, names):
code_lines = ["cdef class " + classname + ":"]
code_lines.extend(create_members_definition(names))
code_lines.append(create_signature(names.keys()))
code_lines.extend(create_initialization(names.keys()))
return "\n".join(code_lines)+"\n"
# utilize cython.inline to generate and load pyx-module:
def create_cnamedtuple_class(classname, names):
code = create_cdef_class_code(classname, names)
code = code + "GenericClass = " + classname +"\n"
ret = cython.inline(code)
return ret["GenericClass"]
可以如下使用,从上面动态定义CTuple
:
CTuple = create_cnamedtuple_class("CTuple",
{"id":"unsigned long long int",
"name":"str",
"isvalid":"bint"})
ob = CTuple(1,"mmm",3)
...
另一种选择是使用 jit-compilation 和 Numba 的 jitted-classes,它们提供了这种可能性。然而,它们似乎要慢得多:
from numba import jitclass, types
spec = [
('id', types.uint64),
('name', types.string),
('isvalid', types.uint8),
]
@jitclass(spec)
class NBTuple(object):
def __init__(self, id, name, isvalid):
self.id = id
self.name = name
self.isvalid = isvalid
结果是:
20.622 s 394.0 MB
所以 numba jitted 类 不是(现在?)一个好的选择。
在 recordclass 库的帮助下,还有另一种快速紧凑的方法:
pip3 install recordclass
import recordclass
TriDO = recordclass.make_dataclass("TriDO",
[('id', int), ('name', str), ('isvalid', bool)],
fast_new=True)
这是性能计数器的值(linux,64 位,python3.9,记录类 >= 0.15):
tuple:
t0 = time.time()
d = {i: (i+1, 'hello', True) for i in range(1000000)}
print('%.3f s %.1f MB' % (time.time()-t0,
psutil.Process(os.getpid()).memory_info().rss / 1024 ** 2))
0.235 s 259.1 MB
dict:
t0 = time.time()
d = {i: {'id':i+1, 'name':'hello', 'isvalid':True} for i in range(1000000)}
print('%.3f s %.1f MB' % (time.time()-t0,
psutil.Process(os.getpid()).memory_info().rss / 1024 ** 2))
0.332 s 428.6 MB
namedtuple:
t0 = time.time()
d = {i: Tri(i+1, 'hello', True) for i in range(1000000)}
print('%.3f s %.1f MB' % (time.time()-t0,
psutil.Process(os.getpid()).memory_info().rss / 1024 ** 2))
1.195 s 275.6 MB
'NamedTuple:'
t0 = time.time()
d = {i: Tri2(i+1, 'hello', True) for i in range(1000000)}
print('%.3f s %.1f MB' % (time.time()-t0,
psutil.Process(os.getpid()).memory_info().rss / 1024 ** 2))
1.059 s 275.2 MB
dataobject:
t0 = time.time()
d = {i: TriDO(i+1, 'hello', True) for i in range(1000000)}
print('%.3f s %.1f MB' % (time.time()-t0,
psutil.Process(os.getpid()).memory_info().rss / 1024 ** 2))
0.256 s 244.2 MB
这里是更准确的时间:
%timeit d = {i:(i+1, 'hello', True) for i in range(1000000)} # tuple
162 ms ± 756 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
%timeit d = {i:{'id':i+1, 'name':'hello', 'isvalid':True} for i in range(1000000)} # dict
250 ms ± 2.77 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
%timeit d = {i:Tri(i+1,'hello',True) for i in range(1000000)} # namedtuple
318 ms ± 422 µs per loop (mean ± std. dev. of 7 runs, 1 loop each)
%timeit d = {i:Tri2(i+1,'hello',True) for i in range(1000000)} # NamedTuple
330 ms ± 5.2 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
%timeit d = {i:TriDO(i+1,'hello',True) for i in range(1000000)} # dataobject
188 ms ± 823 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
您可以保留元组并命名您的索引(下面的第二个选项)
from enum import IntEnum
# int variable
ID, NAME, IS_VALID = 0, 1, 2
# IntEnum
class Index(IntEnum):
ID = 0
NAME = 1
IS_VALID = 2
# Create tuples
d = {i: (i+1, 'hello', True) for i in range(int(1e6))}
t0 = time.time()
# check data access performance
# uncomment only one of these 3 next lines:
# for i in range(len(d)): _ = d[i][0], d[i][1], d[i][2]
# for i in range(len(d)): _ = d[i][ID], d[i][NAME], d[i][IS_VALID]
for i in range(len(d)): _ = d[i][Index.ID], d[i][Index.NAME], d[i][Index.IS_VALID]
print('%.3f s' % (time.time()-t0))
"""
int 0.307 s
int variable 0.312 s
IntEnum 0.749 s
"""
我正在创建一个字典 d
包含一百万个元组项,理想情况下我想通过以下方式访问它们:
d[1634].id # or d[1634]['id']
d[1634].name # or d[1634]['name']
d[1634].isvalid # or d[1634]['isvalid']
而不是 d[1634][0]
、d[1634][1]
、d[1634][2]
,后者不太明确。
根据我的测试:
import os, psutil, time, collections, typing
Tri = collections.namedtuple('Tri', 'id,name,isvalid')
Tri2 = typing.NamedTuple("Tri2", [('id', int), ('name', str), ('isvalid', bool)])
t0 = time.time()
# uncomment only one of these 4 next lines:
d = {i: (i+1, 'hello', True) for i in range(1000000)} # tuple
# d = {i: {'id': i+1, 'name': 'hello', 'isvalid': True} for i in range(1000000)} # dict
# d = {i: Tri(id=i+1, name='hello', isvalid=True) for i in range(1000000)} # namedtuple
# d = {i: Tri2(id=i+1, name='hello', isvalid=True) for i in range(1000000)} # NamedTuple
print('%.3f s %.1f MB' % (time.time()-t0, psutil.Process(os.getpid()).memory_info().rss / 1024 ** 2))
"""
tuple: 0.257 s 193.3 MB
dict: 0.329 s 363.6 MB
namedtuple: 1.253 s 193.3 MB (collections)
NamedTuple: 1.250 s 193.5 MB (typing)
"""
- 与
tuple
相比,使用 - 与
tuple
! 相比,使用
dict
可使 RAM 使用量翻倍
namedtuple
或 NamedTuple
花费的时间乘以 5
问题:Python 3 中是否有类似元组的数据结构,它允许使用 x.id
、x.name
等访问数据,以及RAM 和 CPU 有效率吗?
备注:
在我的实际用例中,
类型的 C 结构tuple
类似于(uint64, uint64, bool)
.我也试过:
slots
(避免内部对象的__dict__
,见Usage of __slots__?)dataclass
:@dataclasses.dataclass class Tri3: id: int ...
ctypes.Structure
:class Tri7(ctypes.Structure): _fields_ = [("id", ctypes.c_int), ...]
但它并没有更好(所有这些都是 ~ 1.2 秒),在性能方面与真正的
tuple
相去甚远以下是其他选项:C-like structures in Python
您可以尝试反转它(存储为数组结构)并以 x['id'][1634]
的形式访问值。换句话说,x
是一个具有三个键的字典,每个键的值是一个列表。这将是 space 高效的。
或者您可以使用 pandas 数据帧。数据帧以矩阵形式存储,其中行具有数字 ID,列具有标签(如 'name' 等字符串)。对于数据框 df
,df.iloc[i]
指向第 $i^th$ 行,您可以通过 df.iloc[i].name
或 df.iloc[i]['name']
Cython 的 cdef-classes 可能是您想要的:它们比纯 Python 类 使用更少的内存,即使它在访问成员时以更多的开销为代价(因为字段存储为 C 值而不是 Python-对象)。
例如:
%%cython
cdef class CTuple:
cdef public unsigned long long int id
cdef public str name
cdef public bint isvalid
def __init__(self, id, name, isvalid):
self.id = id
self.name = name
self.isvalid = isvalid
可以随意使用:
ob=CTuple(1,"mmm",3)
ob.id, ob.name, ob.isvalid # prints (2, "mmm", 3)
Timings/memory消费:
首先,我机器上的基线:
0.258 s 252.4 MB # tuples
0.343 s 417.5 MB # dict
1.181 s 264.0 MB # namedtuple collections
与 CTuple
我们得到:
0.306 s 191.0 MB
几乎一样快,需要的内存要少得多。
如果编译时成员的 C 类型不明确,可以使用简单的 python-objects:
%%cython
cdef class PTuple:
cdef public object id
cdef public object name
cdef public object isvalid
def __init__(self, id, name, isvalid):
self.id = id
self.name = name
self.isvalid = isvalid
时间安排有点令人惊讶:
0.648 s 249.8 MB
我没想到它会比 CTuple
版本慢那么多,但至少它比命名元组快两倍。
这种方法的一个缺点是它需要编译。然而,Cython 提供 cython.inline
可用于编译动态创建的 Cython 代码。
我已经发布了 cynamedtuple
,可以通过 pip install cynamedtuple
安装,它基于下面的原型:
import cython
# for generation of cython code:
tab = " "
def create_members_definition(name_to_ctype):
members = []
for my_name, my_ctype in name_to_ctype.items():
members.append(tab+"cdef public "+my_ctype+" "+my_name)
return members
def create_signature(names):
return tab + "def __init__(self,"+", ".join(names)+"):"
def create_initialization(names):
inits = [tab+tab+"self."+x+" = "+x for x in names]
return inits
def create_cdef_class_code(classname, names):
code_lines = ["cdef class " + classname + ":"]
code_lines.extend(create_members_definition(names))
code_lines.append(create_signature(names.keys()))
code_lines.extend(create_initialization(names.keys()))
return "\n".join(code_lines)+"\n"
# utilize cython.inline to generate and load pyx-module:
def create_cnamedtuple_class(classname, names):
code = create_cdef_class_code(classname, names)
code = code + "GenericClass = " + classname +"\n"
ret = cython.inline(code)
return ret["GenericClass"]
可以如下使用,从上面动态定义CTuple
:
CTuple = create_cnamedtuple_class("CTuple",
{"id":"unsigned long long int",
"name":"str",
"isvalid":"bint"})
ob = CTuple(1,"mmm",3)
...
另一种选择是使用 jit-compilation 和 Numba 的 jitted-classes,它们提供了这种可能性。然而,它们似乎要慢得多:
from numba import jitclass, types
spec = [
('id', types.uint64),
('name', types.string),
('isvalid', types.uint8),
]
@jitclass(spec)
class NBTuple(object):
def __init__(self, id, name, isvalid):
self.id = id
self.name = name
self.isvalid = isvalid
结果是:
20.622 s 394.0 MB
所以 numba jitted 类 不是(现在?)一个好的选择。
在 recordclass 库的帮助下,还有另一种快速紧凑的方法:
pip3 install recordclass
import recordclass
TriDO = recordclass.make_dataclass("TriDO",
[('id', int), ('name', str), ('isvalid', bool)],
fast_new=True)
这是性能计数器的值(linux,64 位,python3.9,记录类 >= 0.15):
tuple:
t0 = time.time()
d = {i: (i+1, 'hello', True) for i in range(1000000)}
print('%.3f s %.1f MB' % (time.time()-t0,
psutil.Process(os.getpid()).memory_info().rss / 1024 ** 2))
0.235 s 259.1 MB
dict:
t0 = time.time()
d = {i: {'id':i+1, 'name':'hello', 'isvalid':True} for i in range(1000000)}
print('%.3f s %.1f MB' % (time.time()-t0,
psutil.Process(os.getpid()).memory_info().rss / 1024 ** 2))
0.332 s 428.6 MB
namedtuple:
t0 = time.time()
d = {i: Tri(i+1, 'hello', True) for i in range(1000000)}
print('%.3f s %.1f MB' % (time.time()-t0,
psutil.Process(os.getpid()).memory_info().rss / 1024 ** 2))
1.195 s 275.6 MB
'NamedTuple:'
t0 = time.time()
d = {i: Tri2(i+1, 'hello', True) for i in range(1000000)}
print('%.3f s %.1f MB' % (time.time()-t0,
psutil.Process(os.getpid()).memory_info().rss / 1024 ** 2))
1.059 s 275.2 MB
dataobject:
t0 = time.time()
d = {i: TriDO(i+1, 'hello', True) for i in range(1000000)}
print('%.3f s %.1f MB' % (time.time()-t0,
psutil.Process(os.getpid()).memory_info().rss / 1024 ** 2))
0.256 s 244.2 MB
这里是更准确的时间:
%timeit d = {i:(i+1, 'hello', True) for i in range(1000000)} # tuple
162 ms ± 756 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
%timeit d = {i:{'id':i+1, 'name':'hello', 'isvalid':True} for i in range(1000000)} # dict
250 ms ± 2.77 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
%timeit d = {i:Tri(i+1,'hello',True) for i in range(1000000)} # namedtuple
318 ms ± 422 µs per loop (mean ± std. dev. of 7 runs, 1 loop each)
%timeit d = {i:Tri2(i+1,'hello',True) for i in range(1000000)} # NamedTuple
330 ms ± 5.2 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
%timeit d = {i:TriDO(i+1,'hello',True) for i in range(1000000)} # dataobject
188 ms ± 823 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
您可以保留元组并命名您的索引(下面的第二个选项)
from enum import IntEnum
# int variable
ID, NAME, IS_VALID = 0, 1, 2
# IntEnum
class Index(IntEnum):
ID = 0
NAME = 1
IS_VALID = 2
# Create tuples
d = {i: (i+1, 'hello', True) for i in range(int(1e6))}
t0 = time.time()
# check data access performance
# uncomment only one of these 3 next lines:
# for i in range(len(d)): _ = d[i][0], d[i][1], d[i][2]
# for i in range(len(d)): _ = d[i][ID], d[i][NAME], d[i][IS_VALID]
for i in range(len(d)): _ = d[i][Index.ID], d[i][Index.NAME], d[i][Index.IS_VALID]
print('%.3f s' % (time.time()-t0))
"""
int 0.307 s
int variable 0.312 s
IntEnum 0.749 s
"""