Can the CPython JIT optimise `getattr` with constant arguments?

Hi,

For the following code:

def f(obj: object, attribute: str):
    return getattr(obj, attribute)

for _ in range(1_000_000_000):
    f(obj, "foo")
    f(obj, "bar")

can the CPython JIT recognise that attribute is always a constant at these call sites, and potentially specialise f() such that the calls become something equivalent to:

for _ in range(1_000_000_000):
    obj.foo
    obj.bar

If so, is there any documentation or way to inspect/verify that this optimisation is happening?

Thanks!

I don’t know if it can currently, but at minimum it would have to check that getattr is the built-in version. Given that caveat, I suppose it could generate the code you want. In theory.

Disclaimer: I know zero about the JIT’s implementation.

Does the JIT optimize across call layers like that? I also know zero about its implementation. If it doesn’t, there’s not a lot of point to this (since getattr(obj, "foo") is going to be pretty rare - the example has the extra layer of indirection to it), but if it does, that’s pretty neat :slight_smile:

If you want to check if the optimization is happening (I really don’t think the JIT can do that), you can use the dis module to look at the bytecode instructions.

There are a couple of ways to verify this optimization is happening. From inside the process you can query the executors, e.g. by using this utility function (it’ll only tell you whether f is JITted, doesn’t work on `getattr``):

import _opcode
import types

def is_jitted(f: types.FunctionType) -> bool:
    for i in range(0, len(f.__code__.co_code), 2):
        try:
            _opcode.get_executor(f.__code__, i)
        except RuntimeError:
            # This isn't a JIT build:
            return False
        except ValueError:
            # No executor found:
            continue
        return True
    return False

From the outside, you can generate debug traces like this:

$ PYTHON_LLTRACE=2 ~/projects/jit_cpython/python -c "class Object:
    def __init__(self):
        self.foo = 42
        self.bar = 24

obj = Object()

def f(obj: object, attribute: str):
    return getattr(obj, attribute)

for _ in range(1_000_000):
    f(obj, 'foo')
    f(obj, 'bar')
" > lltrace.txt

This will create a file with lines like:

Tracing <module> (<string>:1) at byte offset 68 at chain depth 0
   1 ADD_TO_TRACE: _START_EXECUTOR (0, target=55, operand0=0x721685e005ce, operand1=0)
   2 ADD_TO_TRACE: _MAKE_WARM (0, target=0, operand0=0, operand1=0)
0x721685e00490 55: JUMP_BACKWARD(23) 0
   3 ADD_TO_TRACE: _CHECK_VALIDITY (0, target=55, operand0=0, operand1=0)
   4 ADD_TO_TRACE: _SET_IP (0, target=55, operand0=0x721685e005ce, operand1=0)
   5 ADD_TO_TRACE: _CHECK_PERIODIC (0, target=55, operand0=0, operand1=0)
Trace continuing (fitness=2494)
0x721685e00490 34: FOR_ITER_RANGE(21) 0
   6 ADD_TO_TRACE: _CHECK_VALIDITY (0, target=34, operand0=0, operand1=0)
   7 ADD_TO_TRACE: _SET_IP (0, target=34, operand0=0x721685e005a4, operand1=0)
   8 ADD_TO_TRACE: _ITER_CHECK_RANGE (21, target=34, operand0=0, operand1=0)
   9 ADD_TO_TRACE: _GUARD_NOT_EXHAUSTED_RANGE (21, target=58, operand0=0, operand1=0)
  10 ADD_TO_TRACE: _ITER_NEXT_RANGE (21, target=34, operand0=0, operand1=0)
Trace continuing (fitness=2485)
[...]

Asking Claude to analyze the debug output, the calls to getattr are never fully optimized. The lookup for getattr is optimized, the callable becomes a known constant, the call to f is inlined, etc. But what you’d like to see happen doesn’t. Let me quote Claude:

“”"

Why

Python/optimizer_bytecodes.c has constant-folding/specialization rules for exactly six builtins — isinstance, len, type, str, tuple, list.append. getattr is not among them, and the generic rule is a black box:

op(_CALL_BUILTIN_FAST, (callable, self_or_null, args[oparg] -- callable, self_or_null, args[oparg])) {
    callable = sym_new_not_null(ctx);
}

This is true even though the optimizer does know the name statically here: f is inlined into the module trace with 'foo'/'bar' pushed as _LOAD_CONST_INLINE_BORROW. The information is available; nothing consumes it.
“”"