关于性能:什么是Python中最快的(访问)类似结构的对象?

What is the fastest (to access) struct-like object in Python?

我正在优化一些代码,它们的主要瓶颈正在运行,并访问大量类似结构的对象。为了可读性,目前我使用的是namedtuples。但一些使用"TimeIt"的快速基准测试表明,如果性能是一个因素,那么这确实是一种错误的方式:

名为a、b、c的元组:

1
2
>>> timeit("z = a.c","from __main__ import a")
0.38655471766332994

使用__slots__的类,带有a、b、c:

1
2
>>> timeit("z = b.c","from __main__ import b")
0.14527461047146062

带A、B、C键的字典:

1
2
>>> timeit("z = c['c']","from __main__ import c")
0.11588272541098377

使用常量键创建三个值的元组:

1
2
>>> timeit("z = d[2]","from __main__ import d")
0.11106188992948773

使用常量键列出三个值:

1
2
>>> timeit("z = e[2]","from __main__ import e")
0.086038238242508669

三个值的元组,使用本地键:

1
2
>>> timeit("z = d[key]","from __main__ import d, key")
0.11187358437882722

使用本地键列出三个值:

1
2
>>> timeit("z = e[key]","from __main__ import e, key")
0.088604143037173344

首先,这些小的timeit测试是否会使它们失效?我每运行几次,以确保没有任何随机系统事件丢弃它们,结果几乎相同。

字典似乎在性能和可读性之间提供了最佳的平衡,类排在第二位。这是不幸的,因为出于我的目的,我也需要对象的顺序,因此我选择了命名的两个。

列表速度快得多,但常量键是不可维护的;我必须创建一组索引常量,即key_1=1、key_2=2等,这也是不理想的。

我是坚持这些选择,还是有我错过的选择?


要记住的一件事是,命名的双元组被优化为作为元组访问。如果将访问器更改为a[2],而不是a.c,您将看到与元组类似的性能。原因是名称访问器正在有效地转换为对self[idx]的调用,因此需要同时支付索引和名称查找价格。

如果您的使用模式是这样的:按名称访问是常见的,但按元组访问不是常见的,那么您可以编写一个与执行相反操作的NamedDuple等效的快速方法:将索引查找延迟为按名称访问。但是,您将支付索引查找的价格。下面是一个快速实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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__时,时间安排非常糟糕:

1
2
3
4
namedtuple.a  :  0.473686933517
namedtuple[0] :  0.180409193039
struct.a      :  0.180846214294
struct[0]     :  1.32191514969

也就是说,与属性访问的__slots__类的性能相同(毫不奇怪,这就是它的性质),但由于在基于索引的访问中进行了双重查找,因此会受到巨大的惩罚。(值得注意的是,__slots__实际上对速度没有多大帮助。它可以节省内存,但如果没有内存,访问时间几乎相同。)

第三种选择是复制数据,例如列表中的子类,并将值存储在属性和列表数据中。然而,您实际上并没有获得与列表中相同的性能。子类化(引入纯Python重载的检查)有很大的速度冲击。因此,在这种情况下,struct[0]仍然需要大约0.5秒(与原始列表的0.18相比),并且您的内存使用量增加了一倍,因此这可能不值得。


这个问题相当古老(互联网时间),所以我想今天我会尝试复制你的测试,包括常规的cpython(2.7.6)和pypy(2.2.1),看看各种方法之间的比较。(我还为命名的元组添加了索引查找。)

这是一个微基准测试,所以ymmv,但pypy似乎比cpython加快了30倍的命名tuple访问速度(而字典访问速度只加快了3倍)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
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结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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


几个要点和想法:

1)您在一行中多次访问同一索引。您的实际程序可能使用随机或线性访问,这将具有不同的行为。特别是,会有更多的CPU缓存未命中。使用实际程序可能会得到稍微不同的结果。

2)ORDEREDDICTIONARY写在dict周围,因此比dict慢。这不是解决办法。

3)你试过新旧风格的课程吗?(新样式类继承自object;旧样式类不继承)

4)您是否尝试过使用psyco或unladen燕子?

5)您的内部循环是修改数据还是只访问数据?在进入循环之前,可以将数据转换成最有效的形式,但在程序的其他地方使用最方便的形式。


我可能会尝试(a)发明某种特定于工作负载的缓存,并将数据的存储和检索卸载到类似memcachedb的进程,以提高可扩展性,而不是只提高性能,或者(b)使用本机数据存储将数据重写为C扩展。可能是一种有序的字典类型。

你可以从这个开始:http://www.xs4all.nl/~anthon/python/ordereddict/


您可以通过添加__iter____getitem__方法使类具有类似的顺序(可索引和不可重写)。

一个OrderedDict能工作吗?有几个可用的实现,它包含在python31 collections模块中。