20

Why is it that if you compile a conditional expression like

def f():
    if None:
        print(222)
    if 0:
        print(333)

the branches that use numbers get optimized out, but those that use None don't? Example:

 3        0 LOAD_CONST               0 (None)
          3 POP_JUMP_IF_FALSE       14

 4        6 LOAD_CONST               1 (222)
          9 PRINT_ITEM          
         10 PRINT_NEWLINE       
         11 JUMP_FORWARD             0 (to 14)

 5  >>   14 LOAD_CONST               0 (None)
         17 RETURN_VALUE        

In which scenarios could if 0 and if None behave differently?

user541686
  • 205,094
  • 128
  • 528
  • 886
  • 4
    On Python3 it looks to optimize out both. (Obviously that doesn't answer your Python2 question...) – DavidW Jun 06 '17 at 12:23
  • Maybe it's because None is guaranteed to be singleton. It would be cool if we could find relevant docs entry. – Łukasz Rogalski Jun 06 '17 at 12:27
  • 3
    @ŁukaszRogalski: I don't get what being a singleton has to do with this...? – user541686 Jun 06 '17 at 12:28
  • Optimizer can check for identity or maybe have whitelist of valid consts to optimize? Just a wild guess. – Łukasz Rogalski Jun 06 '17 at 12:29
  • @ŁukaszRogalski: "Identity" of what though? Identity only exists at run time. And how can having 1 identity make things harder than having multiple...? (And yes, I imagine there's likely a whitelist somewhere; the question is why isn't `None` on the whitelist when `0` is.) – user541686 Jun 06 '17 at 12:31
  • Wild guess: nobody thought to optimise `if None` out since nobody would actually write that in production code, yet `if 0` matches some generic constant value optimiser. Generically: "because nobody wrote the code that would optimise that case" is always a valid answer. More generically: don't attribute to purpose what can adequately be explained by incompetence. ;) – deceze Jun 06 '17 at 12:44
  • @deceze: That would be my fallback explanation indeed, but it seems too deliberate to just assume it was done out of incompetence. They took care of `0.0` too, not just `0`... – user541686 Jun 06 '17 at 12:51
  • 2
    This question would have been more constructive if you've asked why it's optimized in Python-3 and while not in python-2 or even better why it's like so for 0 and None in python-3. The reason that it behaves like so in python-2 is that they've missed the `None` for this particular optimization case. In both versions `NoneType` is among the built-in constant types and nothing changed. Well, probably someone will post an answer by showing the source code that demonstrates this behavior. – Mazdak Jun 06 '17 at 13:55
  • @Kasramvd: I didn't actually know if it was optimized in Python 3 when I posted it; I knew True/False were treated differently there so thought maybe None is too, and I tagged this as Python 2. I didn't really care to ask about Python 3 either since I don't use it, hence why you see the question as is. – user541686 Jun 06 '17 at 15:43

2 Answers2

8

My guess: It's an oversight that happened because None is just a special-cased name (or global) in python-2.x.

If you take a look at the bytecode-optimizer code in python-2.x:

switch (opcode) {

   /* ... More cases ... */

        /* Replace LOAD_GLOBAL/LOAD_NAME None
           with LOAD_CONST None */
    case LOAD_NAME:
    case LOAD_GLOBAL:
        j = GETARG(codestr, i);
        name = PyString_AsString(PyTuple_GET_ITEM(names, j));
        if (name == NULL  ||  strcmp(name, "None") != 0)
            continue;
        for (j=0 ; j < PyList_GET_SIZE(consts) ; j++) {
            if (PyList_GET_ITEM(consts, j) == Py_None)
                break;
        }
        if (j == PyList_GET_SIZE(consts)) {
            if (PyList_Append(consts, Py_None) == -1)
                goto exitError;
        }
        assert(PyList_GET_ITEM(consts, j) == Py_None);
        codestr[i] = LOAD_CONST;
        SETARG(codestr, i, j);
        cumlc = lastlc + 1;
        break;      /* Here it breaks, so it can't fall through into the next case */

        /* Skip over LOAD_CONST trueconst
           POP_JUMP_IF_FALSE xx. This improves
           "while 1" performance. */
    case LOAD_CONST:
        cumlc = lastlc + 1;
        j = GETARG(codestr, i);
        if (codestr[i+3] != POP_JUMP_IF_FALSE  ||
            !ISBASICBLOCK(blocks,i,6)  ||
            !PyObject_IsTrue(PyList_GET_ITEM(consts, j)))
            continue;
        memset(codestr+i, NOP, 6);
        cumlc = 0;
        break;

   /* ... More cases ... */

}

You may notice that None is loaded with LOAD_GLOBAL or LOAD_NAME and then replaced by LOAD_CONST.

However: After it is replaced it breaks, so it can't go into the LOAD_CONST case in which the block would be replaced with a NOP if the constant isn't True.


In python-3.x the optimizer doesn't need to special case the name (or global) None because it's always loaded with LOAD_CONST and the bytecode-optimizer reads:

switch (opcode) {

   /* ... More cases ... */

        /* Skip over LOAD_CONST trueconst
           POP_JUMP_IF_FALSE xx.  This improves
           "while 1" performance.  */
    case LOAD_CONST:
        CONST_STACK_PUSH_OP(i);
        if (nextop != POP_JUMP_IF_FALSE  ||
            !ISBASICBLOCK(blocks, op_start, i + 1)  ||
            !PyObject_IsTrue(PyList_GET_ITEM(consts, get_arg(codestr, i))))
            break;
        fill_nops(codestr, op_start, nexti + 1);
        CONST_STACK_POP(1);
        break;

   /* ... More cases ... */

}

There's no special case for LOAD_NAME and LOAD_GLOBAL anymore so if None (but also if False - False was also made a constant in python-3.x) will go into the LOAD_CONST case and then replaced by a NOP.

MSeifert
  • 145,886
  • 38
  • 333
  • 352
  • 1
    +1 I think what's convincing me that you're probably right is the fact that this is happening in the *peephole optimizer* -- i.e. at the bytecode level -- rather than at the Python level. I think this is important because Python didn't use to treat None as a literal constant; that only came in version 2.4. If this optimization had been at the AST level, it would've been *pretty darn hard* to forget to treat it like every other literal constant. But it wouldn't trivially randomly occur to you to change the peephole optimizer. Hence this discrepancy. Thanks! – user541686 Jun 07 '17 at 07:18
1

Disclaimer: This is not really an answer, but just a report of my succeeded attempt to override None in CPython 2.7 despite the protection by the compiler.

I found a way of overriding None in CPython 2.7, though it involves a dirty trick and could similarly be done to literals. Namely, I replace the constant entry #0 in the co_consts field of a code object:

def makeNoneTrueIn(func):
    c = func.__code__
    func.__code__ = type(c)(c.co_argcount,
                            c.co_nlocals,
                            c.co_stacksize,
                            c.co_flags,
                            c.co_code,
                            (True, ) + c.co_consts[1:],
                            c.co_names,
                            c.co_varnames,
                            c.co_filename,
                            c.co_name,
                            c.co_firstlineno,
                            c.co_lnotab,
                            c.co_freevars,
                            c.co_cellvars)


def foo():
    if None:
        print "None is true"
    else:
        print "None is false"

foo()
makeNoneTrueIn(foo)
foo()

Output:

None is false
None is true
Leon
  • 31,443
  • 4
  • 72
  • 97
  • Well, that doesn't "change" `None`, it creates a new function in which `None` was replaced with `True` and then replaces the function. – MSeifert Jun 07 '17 at 12:07
  • @MSeifert Agree. But your description of that process can be applied to any code transformation (e.g. optimization) as well. – Leon Jun 07 '17 at 12:28
  • certainly - it was only meant as additional information. I just found the phrase "overriding `None`" a bit vague and wanted to share my thoughts on it. – MSeifert Jun 07 '17 at 13:17