Remix.run Logo
▲ kannanvijayan 6 hours ago

Gonna dump a technical hot take here.

Python is a mature production language with a lot of users. Even before it was huge - like 20 years ago when I was in college - getting a small backwards compatible API change into a python extension module (gnu readline in my case) required convincing GvR personally that the API change was necessary.

Every JIT attempt I've seen for python just tries to cut too deep in the first pass, and scope themselves too large.

I believe any successful attempt at optimizing python performance from JIT will FIRST start with a 100% semantically compatible hidden type model for optimizing object property access and method dispatch.

The single biggest performance delta in most dynamic language jits come from that, not from optimizing guards off of bytecode operations or from compiling to machine code. It's from converting hashtable lookups into constant-time guard + offset lookups.

The hidden type model is also a hard nut to crack. It needs to perserve dict semantics which have very dynamic behaviour via the various `__*__` methods, and handle exotics scope objects since scoped name lookups are the most common ones which have their own exotic behaviours (I remember python providing some ability for callee frames to inspect caller scopes in some limited ways).

▲adrian17 5 hours ago | parent | next [-]

> It's from converting hashtable lookups into constant-time guard + offset lookups.

Python already does it in a lot of cases, see opcode specializations like LOAD_ATTR_INSTANCE_VALUE, LOAD_ATTR_SLOT.

▲kannanvijayan 5 hours ago | parent [-]

I haven't kept up with python development lately, so thanks for pointing that out. I looked this up and it does seem like some sort of property access is being optimized. What's not clear is what the hidden runtime type model is being used for this, or if it's a lightweight tracking that captures some subset of property accesses.

More digging led to this: https://peps.python.org/pep-0659/ which seems to be what the effort is organized around?

Python's object semantics are broadly similar to Javascript, bar the prototypical inheritance chain, but with the extra complexity of potentially needing to handle dynamic `__*attr__` overrides. Optimizing for this would require a tree of hidden map objects much like JS JIT engines use. I dug around the codebase a while back looking for something like that but didn't find it. `OrderedDict` seemed promising but after more investigation that turned out to just be adding property ordering to dictionaries.

The thing is, you can build that entire hidden type structure out, AND optimize against it, without ever having to mess with things like bytecode replacement. Once you have the hidden type model, you can modify the property lookup algorithm in the VM itself (as in cpython c code) to use some annotation structure hanging off of the bytecode to look up inline caches of type-guarded fastpaths. It would be an optimized fastpath that integrated directly into the C code that interprets the GETATTR opcode.

▲adrian17 4 hours ago | parent [-]

> Optimizing for this would require a tree of hidden map objects much like JS JIT engines use.

I don’t really think that’s true or even that relevant , at least in context of classes (especially with slots - the class, by definition, _is_ the “hidden map”) and not generic dicts.

> The thing is, you can build that entire hidden type structure out, AND optimize against it, without ever having to mess with things like bytecode replacement.

Pretty sure the motivation was exactly the opposite - that a simple “if a LOAD_ATTR encounters the same class with slots N times in a row, swap it out for LOAD_ATTR_SLOT with inline cached slot index, and swap back to generic one if something changes” is way easier (both to implement and reason about) than any alternative that’d interact with the object model. (Also it means you skip having to branch between a slow and fast path at all)

> you can modify the property lookup algorithm in the VM itself (as in cpython c code) to use some annotation structure hanging off of the bytecode to look up inline caches of type-guarded fastpaths. It would be an optimized fastpath that integrated directly into the C code that interprets the GETATTR opcode.

Sorry, that sounds a bit vague and not too different from what CPython already is doing.

▲kannanvijayan 3 hours ago | parent [-]

Ah, the `__slots__` feature makes it make a lot more sense.

> Sorry, that sounds a bit vague and not too different from what CPython already is doing.

With slots being available, I can see where I was misled. I went looking for a type structure that modeled the dynamic properties on an object which would be used as a typeguard, but the class itself functions as that if you're restricting the optimization to slotted properties.

Appreciate the clarfication!

▲adrian17 3 hours ago | parent [-]

(Note that this optimization can also trigger for plain classes (that’s LOAD_ATTR_INSTANCE_VALUE), it just needs a bit more guards and is only useful if you don’t abuse instances being dynamic objects. I just used slots as the most obvious example, but there are lots of different op specializations since like 3.11)

▲Pxtl 6 hours ago | parent | prev [-]

It still blows my mind that the big "we're breaking backwards compatibility Python 3000" release didn't do anything to lay the groundwork for this, or any of Python's other structural problems.

▲0xpgm 5 hours ago | parent | next [-]

For a long time Guido wanted CPython to remain simple, as the reference implementation.

Probably also during the 2 to 3 migration, but that goal appears to have now been abandoned.

I wonder how the authors of other implementations like pypy will be able to keep up as the reference implementation becomes more and more complex.

▲seabrookmx an hour ago | parent | next [-]

That question is already answered. pypy only supports 3.11 today and is largely unmaintained.

▲Pxtl 4 hours ago | parent | prev [-]

> the reference implementation.

The problem was he exposed too much of the implementation into the language itself, destroying its ability to be a simple reference implementation that others could re-implement.

▲mohamedkoubaa 3 hours ago | parent [-]

This is slowly being addressed now, each version of Python sees the c API become more opaque.

▲jsmith45 23 minutes ago | parent | next [-]

That's at best a partial fix for the API side. Python side CPython sill exposes a LOT of implementation details, and as a result occasionally projects will choose to rely on some of these details. Then other implementations need to decide whether to also pick the same implementations details and expose them, or use different details, and lack compatibility with any library that depended upon them.

The python documentation has historically also not been especially clear at distinguishing between what the language guarantees, vs what is just implementation details of the cPython implementation. So even if libraries wanted to try to avoid depending on implementation specifics, it isn't always clear what those are.

The net result is that for the end user python the language is effectively: whatever cpython does. This really, really discourages creating alternate implementations, as the only way to be an alternative implementation with good compatibility is to be a living fork of cpython, constantly merging in all the changes from it, while also maintaining your improvements.

▲twoodfin 36 minutes ago | parent | prev [-]

Are there any PEPs that describe this work?

▲jodrellblank 2 hours ago | parent | prev | next [-]

They weren't rebuilding the whole language.

https://peps.python.org/pep-3000/ - PEP 3000 says it will be a derived from the Python 2 codebase and not a complete rewrite (referencing Joel Spolsky's views on complete rewrites and Netscape Navigator).

https://peps.python.org/pep-3099/ - PEP 3099 is changes Guido van Rossum rejected from Python 3 (including := as a binding operator), parallel iteration, programmable syntax, parser going beyond LL(1), changing the for/else construct.

https://peps.python.org/pep-3100/ - PEP 3100 is things he set as goals for it, things like all strings are Unicode and byte arrays are a separate type, optional type annotations, 'exec' and 'print' change from statements to functions, None/True/False become keywords, old-style classes are gone, changing inequality comparison from <> to != , require C99, drop BeOS support, drop old standard library modules, and more.

actually, more changes than I remember.

▲ModernMech 5 hours ago | parent | prev | next [-]

At the time, Python had a clear purpose as an easy to learn "glue" language between other, perhaps more performant languages. There was no reason to make Python as fast because that would muddy its purpose by introducing compromises and complexity. For better or worse Python sat pretty well in its niche.

Today there are far more people using it, and everyone has a different perspective on what it is. No longer is it a niche glue language, now it's a backbone of AI technology at major tech firms, so it's understandable they'd like it to be faster on its own.

▲jeremyjh 6 hours ago | parent | prev [-]

Its possible this could have been the concrete pillar that broke the camel's back and set Python 3 on the path of Perl 6.