【问题标题】:How to use Python 3.4's enums without significant slowdown?如何在不显着减速的情况下使用 Python 3.4 的枚举?
【发布时间】:2015-06-12 22:00:28
【问题描述】:

我正在编写一个井字游戏并使用枚举来表示三个结果——losedrawwin。我认为这会比使用字符串("lose", "win", "draw") 来指示这些值更好。但是使用枚举给了我很大的性能损失。

这是一个最小的例子,我只是引用 Result.lose 或文字字符串 lose

import enum
import timeit
class Result(enum.Enum):
    lose = -1
    draw = 0
    win = 1

>>> timeit.timeit('Result.lose', 'from __main__ import Result')
1.705788521998329
>>> timeit.timeit('"lose"', 'from __main__ import Result')
0.024598151998361573

这比简单地引用全局变量要慢得多。

k = 12

>>> timeit.timeit('k', 'from __main__ import k')
0.02403248500195332

我的问题是:

  • 我知道在 Python 中全局查找比本地查找慢得多。但为什么枚举查找更糟糕?
  • 如何在不牺牲性能的情况下有效地使用枚举?事实证明,枚举查找完全支配了我的井字游戏程序的运行时间。我们可以在每个函数中保存枚举的本地副本,或者将所有内容包装在一个类中,但这两种方法都显得笨拙。

【问题讨论】:

  • 我认为可能是属性检索速度慢。如果您执行 lose = Result.lose 之类的操作,然后针对 lose 进行测试,无论是本地的还是全局的,我认为您会看到可衡量的加速。
  • 谢谢,效果很好。你知道为什么属性查找比全局查找慢得多吗?我知道局部变量存储在一个固定长度的数组中,而全局变量存储在一个字典中,但是与属性有什么关系呢?
  • 我不知道,抱歉。如果不阅读 CPython 源代码,我无法确定地告诉你任何事情。如果我不得不猜测,我会说对象是用关联数组或映射或任何底层实现的(只是一种可能性,不被视为事实),因此属性名称上使用的散列算法可能会有成本这就像哈希表的字符串键,但这都是猜测。无论如何,您现在知道如何在重复查找的情况下将其最小化。本地化ftw。
  • 枚举属性查找非常慢的事实实际上是一个性能错误:bugs.python.org/issue23486,在 Python 3.5 中,这被加速了很多(慢了 3 倍而不是慢了 20 倍)

标签: python performance enums python-3.4


【解决方案1】:

您正在计时计时循环。 完全忽略字符串文字本身

>>> import dis
>>> def f(): "lose"
... 
>>> dis.dis(f)
  1           0 LOAD_CONST               1 (None)
              3 RETURN_VALUE        

这是一个什么都不做的函数。所以计时循环需要0.024598151998361573 秒才能运行 100 万次。

在这种情况下,字符串实际上变成了f函数的文档字符串:

>>> f.__doc__
'lose'

但是如果没有赋值或者是表达式的一部分,CPython 通常会在代码中省略字符串文字:

>>> def f():
...     1 + 1
...     "win"
... 
>>> dis.dis(f)
  2           0 LOAD_CONST               2 (2)
              3 POP_TOP             

  3           4 LOAD_CONST               0 (None)
              7 RETURN_VALUE        

这里 1 + 1 被折叠成一个常量 (2),字符串文字再次消失。

因此,您无法将此与在 enum 对象上查找属性进行比较。是的,查找属性需要循环。但是查找另一个变量也是如此。如果你真的很担心性能,你可以随时缓存属性查找:

>>> import timeit
>>> import enum
>>> class Result(enum.Enum):
...     lose = -1
...     draw = 0
...     win = 1
... 
>>> timeit.timeit('outcome = Result.lose', 'from __main__ import Result')
1.2259576459764503
>>> timeit.timeit('outcome = lose', 'from __main__ import Result; lose = Result.lose')
0.024848614004440606

timeit 测试中,所有变量都是本地变量,因此Resultlose 都是本地查找。

enum 属性查找确实比“常规”属性查找需要更多时间:

>>> class Foo: bar = 'baz'
... 
>>> timeit.timeit('outcome = Foo.bar', 'from __main__ import Foo')
0.04182224802207202

这是因为enum 元类包含一个specialised __getattr__ hook,每次查找属性时都会调用它; enum 类的属性在专门的字典而不是 __dict__ 类中查找。执行该钩子方法和附加属性查找(访问地图)都需要额外的时间:

>>> timeit.timeit('outcome = Result._member_map_["lose"]', 'from __main__ import Result')
0.25198313599685207
>>> timeit.timeit('outcome = map["lose"]', 'from __main__ import Result; map = Result._member_map_')
0.14024519600206986

在井字游戏中,您通常不会担心时间差异微不足道会导致什么结果。当 人类玩家 比你的计算机慢几个数量级时,则不然。人类玩家不会注意到 1.2 微秒或 0.024 微秒之间的差异。

【讨论】:

  • 啊,好吧,所以当不使用枚举时循环占主导地位。但是,引用全局变量 k 和引用 Result.lose 之间的区别是什么?
  • @EliRose:查找确实有成本,是的。因此,在真正重要的地方(例如,在代码的时间关键部分,在循环中),将查找缓存在本地。
  • 我对内部结构比对井字游戏更感兴趣,其中有很多方法可以解决这个问题。为什么属性查找比全局查找慢很多?
  • @EliRose:名称查找是本地,而不是全局。在枚举上查找属性是 2 次查找; Result 查找,然后是 lose 属性查找。 enum 与后者挂钩;执行了一个专门的__getattr__ hook 来处理属性查找,因此还需要更多时间。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2015-12-24
  • 1970-01-01
  • 1970-01-01
  • 2019-02-01
  • 1970-01-01
  • 2017-02-18
  • 2012-01-16
相关资源
最近更新 更多