7

I just realized that CPython seems to treat constant expressions, which represent the same value, differently with respect to constant folding. For example:

>>> import dis
>>> dis.dis('2**66')
  1           0 LOAD_CONST               0 (2)
              2 LOAD_CONST               1 (66)
              4 BINARY_POWER
              6 RETURN_VALUE
>>> dis.dis('4**33')
  1           0 LOAD_CONST               2 (73786976294838206464)
              2 RETURN_VALUE

For the second example constant folding is applied while for the first it is not though both represent the same value. It doesn't seem to be related to the value of the exponent nor to the magnitude of the result since the following expressions are folded as well:

>>> dis.dis('2.0**66')
  1           0 LOAD_CONST               2 (7.378697629483821e+19)
              2 RETURN_VALUE
>>> dis.dis('4**42')
  1           0 LOAD_CONST               2 (19342813113834066795298816)
              2 RETURN_VALUE

Why are the first two expressions treated differently and, more generally, what are the specific rules that CPython follows for constant folding?


Tested on:

$ python3.6 --version
Python 3.6.5 :: Anaconda, Inc.
$ python3.7 --version
Python 3.7.1
a_guest
  • 34,165
  • 12
  • 64
  • 118
  • 2
    There are no rules. There are only implementation details. They have changed before, and they will change again. – user2357112 May 02 '19 at 02:27
  • Also, I think you may have forgotten to test all your examples on both Python 3.6 and Python 3.7, because `dis.dis('2**66')` shows constant folding on 3.6. – user2357112 May 02 '19 at 02:33
  • Wait, no, that's actually a difference between different *patch levels* of 3.6. The behavior changed between 3.6.4 and 3.6.5. – user2357112 May 02 '19 at 02:36
  • I'm running 3.7 on Windows 64-bit and Python 3.7.0 does *not* fold `dis.dis('2**66')` ("Python 3.7.0 (v3.7.0:1bf9cc5093, Jun 27 2018, 04:59:51) [MSC v.1914 64 bit (AMD64)] on win32") - specifically, it seems to fold any power of two with an exponent of 64 or smaller, but switches to using `BINARY_POWER` for any exponent over 64. – Grismar May 02 '19 at 02:39

1 Answers1

9

There are no rules for constant folding. There are only implementation details. They have changed before, and they will change again.

Heck, you can't even talk about the "Python 3 behavior", or the "Python 3.6 behavior", because these implementation details changed between 3.6.4 and 3.6.5. On 3.6.4, the 2**66 example gets constant-folded.

For now, and no one knows how long "for now" will last, the implementation details are that the AST optimizer includes safeguards to prevent spending too much time or memory on constant folding. The safeguard for 2**66 or 4**33 is based on the number of bits in the LHS and the value of the RHS:

if (PyLong_Check(v) && PyLong_Check(w) && Py_SIZE(v) && Py_SIZE(w) > 0) {
    size_t vbits = _PyLong_NumBits(v);
    size_t wbits = PyLong_AsSize_t(w);
    if (vbits == (size_t)-1 || wbits == (size_t)-1) {
        return NULL;
    }
    if (vbits > MAX_INT_SIZE / wbits) {
        return NULL;
    }
}

MAX_INT_SIZE is #defined earlier as 128. Since 2 is a 2-bit number and 4 is a 3-bit number, the estimated result size is smaller for 4**33, so it passes the check and gets constant-folded.

On Python 3.6.5, the implementation details are mostly similar, but this constant folding happens in the bytecode peephole optimizer instead of the AST optimizer, which doesn't exist on 3.6.5.

On Python 3.6.4, the pre-check safeguard doesn't exist. The peephole optimizer will discard too-large constant-folding results after computing them, which results in different thresholds than the pre-checks.

user2357112
  • 260,549
  • 28
  • 431
  • 505
  • Thanks for the answer. For anyone interested this is [the corresponding issue](https://bugs.python.org/issue30416) linked from [the changelog](https://docs.python.org/3.6/whatsnew/changelog.html#id39). Also worth noting similar checks exist for [multiplication](https://github.com/python/cpython/blob/v3.7.3/Python/ast_opt.c#L165), [left shifts](https://github.com/python/cpython/blob/v3.7.3/Python/ast_opt.c#L228) and [string interpolation](https://github.com/python/cpython/blob/v3.7.3/Python/ast_opt.c#L245). I.e. basically anything that could potentially grow unboundedly in (memory) size. – a_guest May 02 '19 at 10:51