我正在优化一些主要瓶颈正在运行的代码并访问一个非常大的类似结构的对象列表。目前我正在使用namedtuples,以提高可读性。但是使用'timeit'的一些快速基准测试表明,在性能是一个因素的情况下,这确实是错误的方法:
使用a,b,c命名的元组:
>>> timeit("z = a.c", "from __main__ import a")
0.38655471766332994
使用__slots__
的类,使用a,b,c:
>>> timeit("z = b.c", "from __main__ import b")
0.14527461047146062
带键a,b,c的字典:
>>> timeit("z = c['c']", "from __main__ import c")
0.11588272541098377
具有三个值的元组,使用常量键:
>>> timeit("z = d[2]", "from __main__ import d")
0.11106188992948773
使用常量键列出三个值:
>>> timeit("z = e[2]", "from __main__ import e")
0.086038238242508669
具有三个值的元组,使用本地密钥:
>>> timeit("z = d[key]", "from __main__ import d, key")
0.11187358437882722
使用本地密钥列出三个值:
>>> timeit("z = e[key]", "from __main__ import e, key")
0.088604143037173344
首先,这些小timeit
测试是否会使它们无效?我跑了几次,以确保没有随机系统事件抛出它们,结果几乎相同。
字典似乎在性能和可读性之间提供了最佳平衡,而类别排在第二位。这是不幸的,因为为了我的目的,我还需要对象是序列式的;因此我选择了namedtuple。
列表速度要快得多,但常量键不可维护;我必须创建一堆索引常量,即KEY_1 = 1,KEY_2 = 2等,这也是不理想的。
我是坚持这些选择,还是有其他我错过的选择?
答案 0 :(得分:47)
要记住的一件事是,namedtuples已经过优化,可以作为元组进行访问。如果您将访问者更改为a[2]
而不是a.c
,则会看到与元组类似的性能。原因是名称访问者有效地转换为对self [idx]的调用,因此同时为索引和支付名称查找价格。
如果您的使用模式是按名称访问是常见的,但是作为元组访问不是,那么可以编写一个快速等效于namedtuple的东西,它以相反的方式执行操作:将索引查找推迟到按名称访问。但是,您将支付索引查找的价格。例如,这是一个快速实现:
def makestruct(name, fields):
fields = fields.split()
import textwrap
template = textwrap.dedent("""\
class {name}(object):
__slots__ = {fields!r}
def __init__(self, {args}):
{self_fields} = {args}
def __getitem__(self, idx):
return getattr(self, fields[idx])
""").format(
name=name,
fields=fields,
args=','.join(fields),
self_fields=','.join('self.' + f for f in fields))
d = {'fields': fields}
exec template in d
return d[name]
但是当必须调用__getitem__
时,时间非常糟糕:
namedtuple.a : 0.473686933517
namedtuple[0] : 0.180409193039
struct.a : 0.180846214294
struct[0] : 1.32191514969
,即与属性访问的__slots__
类相同的性能(不出所料 - 这就是它),但由于在基于索引的访问中进行双重查找而导致巨大的惩罚。 (值得注意的是__slots__
实际上并没有太大的速度。它可以节省内存,但没有它们的访问时间大致相同。)
三分之一的选择是复制数据,例如。列表中的子类,并将值存储在attributes和listdata中。但是,您实际上并没有获得与列表等效的性能。在进行子类化(引入纯python重载检查)时,速度很快。因此,在这种情况下,struct [0]仍然需要大约0.5秒(与原始列表的0.18相比),并且你的内存使用量增加了一倍,所以这可能不值得。
答案 1 :(得分:41)
这个问题相当陈旧(互联网时间),所以我想我今天尝试复制你的测试,包括常规CPython(2.7.6)和pypy(2.2.1),看看如何比较各种方法。 (我还在索引的查找中添加了命名元组。)
这是一个微观基准,所以YMMV,但pypy似乎加速命名元组访问速度比CPython高30倍(而字典访问速度只增加了3倍)。
from collections import namedtuple
STest = namedtuple("TEST", "a b c")
a = STest(a=1,b=2,c=3)
class Test(object):
__slots__ = ["a","b","c"]
a=1
b=2
c=3
b = Test()
c = {'a':1, 'b':2, 'c':3}
d = (1,2,3)
e = [1,2,3]
f = (1,2,3)
g = [1,2,3]
key = 2
if __name__ == '__main__':
from timeit import timeit
print("Named tuple with a, b, c:")
print(timeit("z = a.c", "from __main__ import a"))
print("Named tuple, using index:")
print(timeit("z = a[2]", "from __main__ import a"))
print("Class using __slots__, with a, b, c:")
print(timeit("z = b.c", "from __main__ import b"))
print("Dictionary with keys a, b, c:")
print(timeit("z = c['c']", "from __main__ import c"))
print("Tuple with three values, using a constant key:")
print(timeit("z = d[2]", "from __main__ import d"))
print("List with three values, using a constant key:")
print(timeit("z = e[2]", "from __main__ import e"))
print("Tuple with three values, using a local key:")
print(timeit("z = d[key]", "from __main__ import d, key"))
print("List with three values, using a local key:")
print(timeit("z = e[key]", "from __main__ import e, key"))
Python结果:
Named tuple with a, b, c:
0.124072679784
Named tuple, using index:
0.0447055962367
Class using __slots__, with a, b, c:
0.0409136944224
Dictionary with keys a, b, c:
0.0412045334915
Tuple with three values, using a constant key:
0.0449477955531
List with three values, using a constant key:
0.0331083467148
Tuple with three values, using a local key:
0.0453569025139
List with three values, using a local key:
0.033030056702
PyPy结果:
Named tuple with a, b, c:
0.00444889068604
Named tuple, using index:
0.00265598297119
Class using __slots__, with a, b, c:
0.00208616256714
Dictionary with keys a, b, c:
0.013897895813
Tuple with three values, using a constant key:
0.00275301933289
List with three values, using a constant key:
0.002760887146
Tuple with three values, using a local key:
0.002769947052
List with three values, using a local key:
0.00278806686401
答案 2 :(得分:3)
一些观点和想法:
1)您计时连续多次访问相同的索引。您的实际程序可能使用随机或线性访问,这将具有不同的行为。特别是,将有更多的CPU缓存未命中。使用实际程序可能会得到略有不同的结果。
2)OrderedDictionary被写为dict
的包装器,因为它比dict
慢。这是一个非解决方案。
3)您是否尝试过新式和旧式课程? (新式类继承自object
;旧式类不继承)
4)您是否尝试过使用psyco或Unladen Swallow?
5)你的内循环是修改数据还是只是访问它?在进入循环之前,可以将数据转换为最有效的形式,但在程序的其他地方使用最方便的形式。
答案 3 :(得分:1)
我很想(a)发明某种工作负载特定的缓存,并将我的数据的存储和检索卸载到类似memcachedb的进程,以提高可伸缩性而不是单独的性能或(b)重写为C扩展,具有本机数据存储。也许是有序字典类型。
答案 4 :(得分:1)
此问题可能很快就会过时。 CPython开发人员显然已大大改善了通过属性名称访问命名元组值的性能。更改定于Python 3.8于2019年10月底发布。
请参阅:https://bugs.python.org/issue32492和https://github.com/python/cpython/pull/10495。
答案 5 :(得分:-1)
您可以通过添加__iter__
和__getitem__
方法来创建类序列,以使它们像序列一样(可索引和可迭代。)
OrderedDict
会有效吗?有几种可用的实现,它包含在Python31 collections
模块中。