Live data from Hacker News

How to make a fast dynamic language interpreter

zef-lang.dev

51–60 of 67 posts

Re: How to make a fast dynamic language interpreter

#51
post #15

Earlier quoted context omitted.

I suppose it depends on where you are looking for dynamicity. In some ways, lua is much more laissez faire of course. But in Python, everything is an object, which is why, as I said, it spends much of its time looking things up. And things like bindings for closures are late, so that's more lookups as well. In lua, many things aren't objects, and, for example, you can add two numbers without looking anything up. Anot…

I think you’re describing deficiencies in the Python impl not anything about the language

> I think you’re describing deficiencies in the Python impl not anything about the language

To some extent, sure. And, looking at your implementation of your language, something like the optimizations on passing small numbers of parameters could probably help Python out. It spends an inordinate amount of time packing and unpacking parameter tuples.

But, for example, you can easily create a subclass of an integer and alter a small portion of its behavior, without having to code every single operation, which I don't think you can do in lua.

So, the dynamicity I'm describing is what the language has to do (more work at runtime) to support its own semantics.

Don't get me wrong. There are certainly opportunities to make Python go faster, and the core team is working on some of them (for example, one optimization is similar to your creation of additional subtree nodes for attribute lookup for known cases, but in bytecode instead), but I also think that the semantics of Python make large classes of optimization more difficult than for other languages.

For a major example of this kind of dynamicity, lua doesn't chain metatables when looking up metamethods, but Python will look stuff up in as many tables as you have subclasses, and has the complexity of dealing with MRO. That's not something that couldn't be JITed, but the edge cases of what you need to update if someone decides to add or modify a method in a superclass get pretty hairy pretty quickly.

Whereas, in lua, if you want to modify a metamethod and have it affect a particular object, yes, absolutely, you can do that, but it is up to you to modify the direct metatable of the object, rather than some ancestor, because lua is not going to dynamically follow the chain of references on every lookup.

And, back to to the parameter optimization case, I haven't thought that much about it, but there are a lot of Python edge cases in parameter passing, that might make that difficult.

And, of course, the use of ref counting instead of mark/sweep has a cost, but people don't like, e.g., PyPy, because their __del__ methods aren't guaranteed to be called immediately when the object goes out of scope. Lua is more like PyPy in this respect.

So Python has a lot of legacy decisions that make optimization harder.

Then things that try to be called Python often take shortcuts that make things faster, but don't get any traction, because they aren't 100% compatible.

So cPython is a Schelling point with semantics that are more complicated than some other language Schelling points, with enough momentum that it becomes difficult for other Python implementations to keep up with the standard, while simultaneously having enough inertia to keep people engaged in using it even though the optimizations are coming slowly.

I think the sPy language (discussed here a few weeks ago) has the right idea. "Hey, we're not Python, but if you like Python you might like us." Things that claim to be Python but faster either wither on the vine because of incompatibilities with cPython, or quickly decide they aren't really Python after they've lost their, ahem, Mojo, or both.

(The primary exception to this is microPython, which has a strong following because it literally can go where no other Python can go.)

Re: How to make a fast dynamic language interpreter

#52
post #36

Earlier quoted context omitted.

CPython current state is more a reflection of resources spent, than what is possible. See experience with Smalltalk and Self, where everything is dynamic dispatch, everything is an object, in a live image that can be monkey patched at any given second. PyPy and GraalPy, and the oldie IronPython, are much better experiences than where CPython currently stands on.

The problem is that AI has been dominating the conversation for so many years, and they'll get more improvements from removing the GIL than they would from adopting the PyPy JIT. The JIT would help everyone else more than removing the GIL, I wish PyPy became the reference implementation during 2.7

Actually because AI has been driving the conversation that CPython JIT efforts are finally happening and being upstreamed.

It is also because of AI, that Intel, AMD and NVidia are now getting serious about Python GPU JITs, that allow writing kernels in a Python subset.

To the point that I bet Mojo will be too late to matter.

Re: How to make a fast dynamic language interpreter

#53

Earlier quoted context omitted.

Lua is way more dynamic

To illustrate this, here's the contorted Lua code from https://news.ycombinator.com/item?id=11327201 local t = setmetatable({}, { __index = pcall, __newindex = rawset, __call = function(t, i) t[i] = 42 end, }) for i=1,100 do assert(t[i] == true and rawget(t, i) == 42) end Arguably this exercises only the slow paths of the VM. A more nuanced take is that Lua has many happy fast paths, whereas Python has some unfortuna…

> A more nuanced take is that Lua has many happy fast paths, whereas Python has some unfortunate semantic baggage that complicates those.

This is a good way to describe it. Most of the semantic baggage doesn't make some speed improvements, up to and including JITing, impossible, but it certainly complicates them.

And of course, any semantic baggage will be useful to someone.

https://xkcd.com/1172/

Re: How to make a fast dynamic language interpreter

#54
This is very interesting and well done.

I've gone through something similar, but for a more functional language (a Scheme). It's interesting how here the biggest wins are from optimizing the objects, while the biggest wins in my case were optimizing closures. The optimizations were very similar.

"Three implementation models for scheme" gives all the answers to make a fast enough scheme, though it has something of a compilation step, so it's not interpreting the original AST.

https://www.cs.unm.edu/~williams/cs491/three-imp.pdf

Re: How to make a fast dynamic language interpreter

#55

Earlier quoted context omitted.

That’s basically what is done all the time in languages where monkey patching is accepted as idiomatic, notably Ruby. Ruby is not known for its speed-first mindset though. On the other side, having a type holding a closed set of applicable functions is somehow questioning. There are languages out there that allows to define arbitrary functions and then use them as a methods with dot notation on any variable matching…

> Ruby is not known for its speed-first mindset though. Or its maintainability, and this is one of the big reasons why. Methods and variables are dynamically generated at runtime which makes it impossible to even grep for them. If you have a large Ruby codebase (say Gitlab or Asciidoctor), it can be almost impossible to trace through code unless you are familiar with the entire codebase. Their "answer" is that you ru…

That's yet an other topic, as monkey patching can definitely be explicit in ruby. The dynamically generated things at runtime are generally through the catch all method missing facility that can be overwritten. This can also be done in, say, PHP. It just that the community is less fond of it. Not sure about what most popular ahead of time oriented languages expose as facility in this area, obviously one can always even decide to generate automodifying executable. There is nothing special about ruby when it comes to go into forbidden realms, except maybe it doesn't come to much in your way when you try to express something, even if that is not the most maintenance friendly path.

Re: How to make a fast dynamic language interpreter

#56
post #29

The jump from change #5 to #6 (inline caches + hidden-class object model) doing the bulk of the work here really tracks with how V8/JSC got fast historically — dynamic dispatch on property access is where naive interpreters die, and everything else is kind of rounding error by comparison. Nice that it's laid out so you can see the contribution of each step in isolation; most perf writeups just show the final number.

I agree, but there’s a tiny caveat that this is for one specific benchmark that, I think, doesn’t reflect most real-world code. I’m basing that on the 1.6% improvement they got on speeding up sqrt . That surprised me, because, to get such an improvement, the benchmark must spend over 1.6% of its time in there, to start with. Looking in the git repo, it seems that did happen in the nbody simulation ( https://github.co…

Before that specialization, sqrt calls were hilariously slow - so even calling it sparingly could significantly impact performance.

Basically the flow was:

- check if we’re calling a method of an object

- nope, ok, so cascade through 10+ symbol comparisons

- sqrt was towards the bottom of the cascade

Re: How to make a fast dynamic language interpreter

#57
Good writeup. The Arguments arc (#7→#13) hits close — did basically the same dance for an async step evaluator in Rust a while back. Went all in on Cow assuming borrow-in-the-common-case would earn its keep. Microbenches looked great. Real workload: the Cow discriminant plus lifetime gunk bled into every combinator past the first await, inlining fell off a cliff, the whole point of Cow evaporated. Ripped it out for NoInput / OneInput / MultiInput(Vec) at the evaluator boundary — same split as your ZeroArguments / OneArgument / TwoArguments, just arrived at the ugly way. One thing I keep wondering: have you stacked arity specialization with type specialization on the native path? Binary style, drops the isInt probe altogether. Guessing the code size math didn't work out, or ICs are already soaking up whatever's hot on the object side so the native fast paths don't matter much. Which one?

Re: How to make a fast dynamic language interpreter

#58
Really interesting read, especially after I released the initial version of my own interpreter which is also an AST-walking interpreter. My main goal was to understand at a basic level what it takes to build an interpreted programming language.

I didn't want any optimisation complexities and just focused on being able to understand my own Rust code. I was surprised by the performance I got simply by using my favourite language and as a bonus, since Rust takes care of all the ownership and lifetimes, I don't need a garbage collector. For sure, right now I'm being super conservative and rely on cloning stuff to avoid lifetime hell in stuff like closures, but the speed and memory profile is still very decent.

For anyone interested in a simple to understand tree-walking interpreter in Rust, which is heavily based in expressive enums where code is data, here's my interpreter:

https://gluonscript.org/

Re: How to make a fast dynamic language interpreter

#59
post #19

In a similar vein, see this page about the performance of the interpreter for the dynamic language Wren: https://wren.io/performance.html Unlike the Zef article, which describes implementation techniques, the Wren page also shows ways in which language design can contribute to performance. In particular, Wren gives up dynamic object shapes, which enables copy-down inheritance and substantially simplifies (and hence a…

Yes, language design is a hugely important determinant of interpreter or JIT speed. There are many highly optimised VMs for dynamic languages but LuaJIT is king because Lua is such a small and suitable language, and although it does have a couple difficult to optimise features, they are few enough that you can expend the effort. It's nothing like Python. It's not much of an exaggeration to say Python is designed to m…

That is an incorrect analysis. CPython is difficult to JIT because of the lack of thought to the native bindings / extensions, not because of the language itself (as others point out PyPy was way faster long ago)

Re: How to make a fast dynamic language interpreter

#60

Earlier quoted context omitted.

Yes, language design is a hugely important determinant of interpreter or JIT speed. There are many highly optimised VMs for dynamic languages but LuaJIT is king because Lua is such a small and suitable language, and although it does have a couple difficult to optimise features, they are few enough that you can expend the effort. It's nothing like Python. It's not much of an exaggeration to say Python is designed to m…

That is an incorrect analysis. CPython is difficult to JIT because of the lack of thought to the native bindings / extensions, not because of the language itself (as others point out PyPy was way faster long ago)

You're correct. I neglected that; extension API compatibility is a big (the most important?) difference between PyPy and CPython's JIT. Amongst language features that affect optimisation potential, an extension API can be the worst.

Edit: I think what you're alluding to is that tracing JITs can overcome a lot of dynamic language features which make things hopeless for method JITs. Where LuaJIT really shines vs PyPy is outside of JITed loops. (Also memory and compile overheads). I realise this is a bit of a motte and bailey.

Post reply on HN