Search code examples
pythonoptimizationpython-internals

How does this internal Python optimization work for mathematical expressions?


This is an optimization question. I have an expression inside a function like the following:

>>> def x():
...     num = 2 * 4 * 100 * 20
...
>>> x.__code__.co_consts
(None, 2, 4, 100, 20, 8, 800, 16000)

The result of the expression 2 * 4 * 100 * 20 is 16000, so if we disassemble the x:

>>> dis.dis(x)
  2           0 LOAD_CONST               7 (16000)
              3 STORE_FAST               0 (x)
              6 LOAD_CONST               0 (None)
              9 RETURN_VALUE       

The 16000 is pretty much what's need. co_consts stores 8 and 800 which are technically not needed anymore we have the total or are they?

Comparing the above expression with another one:

>>> def x():
...     num = 3 + 4 + 9  * 4
... 
>>> x.__code__.co_consts
(None, 3, 4, 9, 7, 36)

looks like the bytecode compiler takes binary operands and stores their computation values:

9 * 4   36 
3 + 4   7 

disassembling the function:

>>> dis.dis(x)
  2           0 LOAD_CONST               4 (7)
              3 LOAD_CONST               5 (36)
              6 BINARY_ADD          
              7 STORE_FAST               0 (num)
             10 LOAD_CONST               0 (None)
             13 RETURN_VALUE   

Interestingly, if you take this expression: 2 + 5 * 8 - 5 + 23 * 4, co_consts will be (None, 2, 5, 8, 23, 4, 40, 92) only the multiplications were computed: 5 * 8 and 23 * 4 the addition and subtraction were ignored.

How does this optimization really work? I tested this only on 2.7.


Solution

  • There's no explaining this ;-) Meaning that it's a dark corner entirely reflecting implementation details. For better or worse, the "peephole optimizer" responsible works not on a parse tree of the program, but on the generated byte code. This is clumsy to work with, so it's hard to predict what will happen, and it changes across releases. For example, here under Python 3.6.1:

    >>> def f():
    ...     return 2 + 5 * 8 - 5 + 23 * 4
    >>> f.__code__.co_consts
    (None, 2, 5, 8, 23, 4, 40, 42, 37, 92, 129)
    >>> import dis
    >>> dis.dis(f)
      2           0 LOAD_CONST              10 (129)
                  2 RETURN_VALUE
    

    So the expression was collapsed to its final value, but all the intermediate values were left behind in the tuple of constants.

    @COLDSPEED already linked to the source code in their comment, and the only real answer to any question about it has to come from staring at the peephole.c used in the version of CPython you're running. Over time it's slowly gotten more ambitious, but there's no real plan behind it.