Python中访问速度最快的类结构对象是什么?
我正在优化一些代码,主要的问题是处理和访问一个非常大的结构体列表。目前我使用的是命名元组,因为这样更容易阅读。但是通过使用'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
测试有没有什么问题会让它们不准确?我多次运行每个测试,以确保没有随机的系统事件影响结果,结果几乎是一样的。
看起来字典在性能和可读性之间提供了最佳的平衡,类则排在第二。这让我有点失望,因为对我来说,我还需要这个对象像序列一样;所以我选择了命名元组。
列表的速度明显更快,但常量键难以维护;我得创建一堆索引常量,比如KEY_1 = 1, KEY_2 = 2等等,这也不是理想的选择。
我是不是只能在这些选择中徘徊,还是说我错过了什么替代方案?
7 个回答
这个问题可能很快就不再存在了。CPython的开发者们显然对通过属性名称访问命名元组的性能做了很大的改进。这些改动计划在Python 3.8中发布,预计在2019年10月底推出。
详细信息请查看:https://bugs.python.org/issue32492 和 https://github.com/python/cpython/pull/10495。
这个问题已经有点时间了(在互联网的时间里),所以我今天决定试着重复一下你的测试,分别用普通的CPython(2.7.6)和pypy(2.2.1)来看看不同的方法比较起来怎么样。(我还加了一个对命名元组的索引查找。)
这算是一个小规模的基准测试,所以结果可能会有所不同,但看起来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
需要注意的一点是,命名元组(namedtuples)在作为元组访问时是经过优化的。如果你把访问方式改成 a[2]
而不是 a.c
,你会发现它的性能和普通元组差不多。原因是,使用名称访问时实际上是调用了自定义的索引方法 self[idx],所以你需要同时付出索引和名称查找的代价。
如果你的使用方式是通过名称访问很常见,但作为元组访问不常见,你可以考虑写一个快速的替代品,做相反的事情:把索引查找推迟到通过名称访问时再进行。不过,这样的话,你就得为索引查找付出代价了。例如,这里有一个简单的实现:
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__
在速度上其实帮助不大。它节省了内存,但访问时间和没有使用它们时差不多。
还有第三种选择,就是复制数据,比如从列表(list)继承,并同时在属性和列表数据中存储值。不过这样你并不能得到和列表一样的性能。因为子类化会带来速度上的损失(需要进行纯 Python 重载的检查)。因此,在这种情况下,struct[0] 仍然需要大约 0.5 秒(而原始列表只需 0.18 秒),而且你会使用双倍的内存,所以这样做可能不太划算。