【问题标题】:What is the fastest (to access) struct-like object in Python?Python 中最快(访问)类似结构的对象是什么?
【发布时间】:2011-02-08 10:10:09
【问题描述】:

我正在优化一些代码,其主要瓶颈在于运行并访问非常大的类似结构的对象列表。目前我正在使用命名元组,以提高可读性。但是一些使用“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 等,这也不理想。

我是否坚持这些选择,或者我错过了其他选择?

【问题讨论】:

  • 如果性能如此重要,为什么不使用 C?
  • @Skilldrick:这只是一个较大程序的一小部分,它受益于用 Python 编写。将这部分重写为 C 扩展是一种选择,但有些不可取,因为其他代码也涉及数据,使事情变得有点复杂。性能很重要,但不是那么至关重要;如果不是因为降低了可维护性,我会对列表提供的 4 倍改进感到非常满意。在决定走哪条路之前,我只是在寻找其他选择。
  • @Warren P:是的;我不会过早地优化。这是一个非常紧凑的循环,其中简单地访问结构是工作的重要部分。这是程序中最慢的剩余循环。即使是适度的改进也可以将实际运行时间缩短一两秒。由于整个事情是重复的,所以加起来。
  • 也可以考虑尝试 pypy。使用 pypy,我在不同案例之间的表现没有任何差异。
  • numpy 具有某种结构,在某些情况下可以提供比 C 更好的性能。 docs.scipy.org/doc/numpy/user/basics.rec.html这个和YMMV我没试过!

标签: python performance data-structures


【解决方案1】:

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

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

【讨论】:

    【解决方案2】:

    您可以通过添加__iter____getitem__ 方法来使您的类像序列一样,使它们像序列一样(可索引和可​​迭代)。

    OrderedDict 有用吗?有几种可用的实现,它包含在 Python31 collections 模块中。

    【讨论】:

      【解决方案3】:

      几点和想法:

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

      2. OrderedDictionary 是作为dict 的包装器编写的,因此它会比dict 慢。这是一个非解决方案。

      3. 您是否尝试过新式和旧式课程? (新式类继承自object;旧式类不继承)

      4. 您是否尝试过使用psycoUnladen Swallow? (2020 年更新 - 这两个项目已死)

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

      【讨论】:

        【解决方案4】:

        要记住的一点是,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__ 在速度方面实际上并没有太大帮助。它节省了内存,但没有它们的访问时间大致相同。)

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

        【讨论】:

        • 小心这个配方,其中字段可能有用户输入数据——字段上的 exec 可以运行任意代码。否则超级酷。
        • namedtuples 按名称访问比按索引访问慢是不是很傻?如果实现一个 NAMEDtuple,我为什么要优化索引访问?
        【解决方案5】:

        这个问题相当老了(互联网时间),所以我想我今天会尝试使用常规 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
        

        【讨论】:

        • 有趣的是 pypy 最糟糕的是字典。
        【解决方案6】:

        这个问题可能很快就会过时。 CPython 开发人员显然对通过属性名称访问命名元组值的性能进行了重大改进。这些更改计划于 2019 年 10 月末在Python 3.8 发布。

        请参阅:https://bugs.python.org/issue32492https://github.com/python/cpython/pull/10495

        【讨论】:

        • 感谢您的信息!事实上,引用 docs.python.org/3/whatsnew/3.8.html 的内容:“在 collections.namedtuple() 中加快了字段查找速度。现在它们的速度提高了两倍多,使其成为 Python 中最快的实例变量查找形式。”
        • 这个答案应该被投票以获得更多关注。
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2017-12-11
        • 1970-01-01
        相关资源
        最近更新 更多