I was writing a tic-tac-toe game and using an Enum to represent the three outcomes -- lose
, draw
, and win
. I thought it would be better style than using the strings ("lose", "win", "draw")
to indicate these values. But using enums gave me a significant performance hit.
Here's a minimal example, where I simply reference either Result.lose
or the literal string 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
This is much slower than simply referencing a global variable.
k = 12
>>> timeit.timeit('k', 'from __main__ import k')
0.02403248500195332
My questions are:
- I know that global lookups are much slower than local lookups in Python. But why are enum lookups even worse?
- How can enums be used effectively without sacrificing performance? Enum lookup turned out to be completely dominating the runtime of my tic-tac-toe program. We could save local copies of the enum in every function, or wrap everything in a class, but both of those seem awkward.
You are timing the timing loop. A string literal on its own is ignored entirely:
That's a function that does nothing at all. So the timing loop takes
0.024598151998361573
seconds to run 1 million times.In this case, the string actually became the docstring of the
f
function:but CPython generally will omit string literals in code if not assigned or otherwise part of an expression:
Here the
1 + 1
as folded into a constant (2
), and the string literal is once again gone.As such, you cannot compare this to looking up an attribute on an
enum
object. Yes, looking up an attribute takes cycles. But so does looking up another variable. If you really are worried about performance, you can always cache the attribute lookup:In
timeit
tests all variables are locals, so bothResult
andlose
are local lookups.enum
attribute lookups do take a little more time than 'regular' attribute lookups:That's because the
enum
metaclass includes a specialised__getattr__
hook that is called each time you look up an attribute; attributes of anenum
class are looked up in a specialised dictionary rather than the class__dict__
. Both executing that hook method and the additional attribute lookup (to access the map) take additional time:In a game of Tic-Tac-Toe you don't generally worry about what comes down to insignificant timing differences. Not when the human player is orders of magnitude slower than your computer. That human player is not going to notice the difference between 1.2 microseconds or 0.024 microseconds.