Live data from Hacker News

Performance of the Python 3.14 tail-call interpreter

blog.nelhage.com

31–40 of 180 posts

Re: Performance of the Python 3.14 tail-call interpreter

#31

Benchmarking is just insanely hard to do well. There are so many things which can mislead you. I recently discovered a way to make an algorithm about 15% faster. At least, that's what all the benchmarks said. At some point I duplicated the faster function in my test harness, but did not call the faster version, just the original slower one... And it was still 15% faster. So code that never executed sped up the origin…

That linker lottery led to a 15% improvement? I'm surprised. Do you know in what cases you get such a big improvement from something like that? Is it rare? How did you end up reasoning about it?

Various research has shown that the variation can be much higher than 15% due to things like this. It's not that rare; I keep bumping into it. Compilers and linkers do a reasonable job but fundamentally modern CPUs are extremely complex beasts.

I found Casey Muratori's series the best explanation of what is going on at the CPU level.

Re: Performance of the Python 3.14 tail-call interpreter

#32
post #14

Benchmarking is just insanely hard to do well. There are so many things which can mislead you. I recently discovered a way to make an algorithm about 15% faster. At least, that's what all the benchmarks said. At some point I duplicated the faster function in my test harness, but did not call the faster version, just the original slower one... And it was still 15% faster. So code that never executed sped up the origin…

I vaguely remember about some benchmarking project that deliberately randomised these compiler decisions, so that they could give you more stable estimates of how well your code actually performed, and not just how well you won or lost the linker lottery.

The Coz profiler from Emery Berger.

It can actually go a step further and give you decent estimate of what functions you need to change to have the desired latency/throughput increases.

Re: Performance of the Python 3.14 tail-call interpreter

#33
post #14

Earlier quoted context omitted.

I vaguely remember about some benchmarking project that deliberately randomised these compiler decisions, so that they could give you more stable estimates of how well your code actually performed, and not just how well you won or lost the linker lottery.

The Coz profiler from Emery Berger. It can actually go a step further and give you decent estimate of what functions you need to change to have the desired latency/throughput increases.

Thanks, I was trying to remember that one!

Re: Performance of the Python 3.14 tail-call interpreter

#34

Earlier quoted context omitted.

> This is a very good example of how C is not "close to the machine" or > "portable assembly", C is very much "portable assembly" from the perspective of other systems programming languages of the 80s-90s era. The C expression `a += 1` can be trusted to increment a numeric value, but the same expression in C++ might allocate memory or unwind the call stack or do who knows what. Similarly, `a = "a"` is a simple pointe…

When the code has hit the IR in clang or gcc, there is no 'a' (we know that with certainty, since SSA form doesn't mutate but assigns to fresh variables). We don't know if there will be an increment of 1, the additions could be coalesced (or elided if the result can be inferred another way). The number can even decrease, say if things have been handled in chunks of 16, and needs to be adjusted down in the last chunk.…

From a high-level academic view, yes, the compiler is allowed to perform any legal transformation. But in practice C compilers are pretty conservative about what they emit, especially when code is compiled without -march= .

You don't have to take my word for it. Go find a moderately complex open-source library written in C, compile it, then open up the result in Hexrays/Ghidra/radare2/whatever. Compare the compiled functions with their original source and you'll see there's not that much magic going on.

Re: Performance of the Python 3.14 tail-call interpreter

#35
post #20

Earlier quoted context omitted.

I know that for 'int a' the statement 'a += 1' can give rather surprising results. And you made a universal statement that 'a += 1' can be trusted. Not just that it can sometimes be trusted. In C++ the code you gave above can also be trusted as far as I can tell. At least as much as the C version.

a+=1 will not produce any surprising results, signed integer overflow is well defined on all platforms that matter. And we all know about the looping behavior, it isn't surprising. The only surprising part would be if the compiler decides to use inc vs add, not that it really matters to the result.

> a+=1 will not produce any surprising results, signed integer overflow is well defined on all platforms that matter.

I'm not sure what you are talking about?

There's a difference between how your processor behaves when given some specific instructions, and what shenanigans your C compiler gets up to.

See eg https://godbolt.org/z/YY69Ezxnv and tell me where the ADD instruction shows up in the compiler output. Feel free to pick a different compiler target than Risc-V.

Re: Performance of the Python 3.14 tail-call interpreter

#36

This is a very good example of how C is not "close to the machine" or "portable assembly", modern optimizers will do drastic changes to the logic as long as it has no observable effect. As stated in the post: "Thus, we end up in this odd world where clang-19 compiles the computed-goto interpreter “correctly” – in the sense that the resulting binary produces all the same value we expect – but at the same time it produ…

> This is a very good example of how C is not "close to the machine" or > "portable assembly", C is very much "portable assembly" from the perspective of other systems programming languages of the 80s-90s era. The C expression `a += 1` can be trusted to increment a numeric value, but the same expression in C++ might allocate memory or unwind the call stack or do who knows what. Similarly, `a = "a"` is a simple pointe…

> The phrase "C is portable assembly" isn't a claim that each statement gets compiled directly to equivalent machine code.

Weasel words. Like a "self driving car" that requires a human driver with constant attention willing to take over within a few hundred milliseconds.

People advocate for C and use it in a way that implies they think it can achieve specific machine outcomes, and it usually does .. except when it doesn't. If people want a portable assembler they should build one.

Re: Performance of the Python 3.14 tail-call interpreter

#37
post #28

Earlier quoted context omitted.

Show me a C compiler that miscompiles the following code and I'll concede the point: uint32_t add_1(uint32_t a) { a += 1; return a; }

C might be low level from the perspective of other systems languages, but that is like calling Apollo 11 simple from the perspective of modern spacecraft. C as written is not all that close to what actually gets executed. For a small example, there are many compilers who would absolutely skip incrementing 'a' in the following code: uint32_t add_and_subtract_1(uint32_t a) { a += 1; a -= 1; return a; } Even though that…

> It gets even worse in bigger functions where out-of-order execution starts being a thing.

In addition, add that your processor isn't actually executing x86 (nor ARM etc) instructions, but interprets/compiles them to something more fundamental.

So there's an additional layer of out-of-order instructions and general shenanigans happening. Especially with branch prediction in the mix.

Re: Performance of the Python 3.14 tail-call interpreter

#38
post #22

Earlier quoted context omitted.

Show me a C compiler that miscompiles the following code and I'll concede the point: uint32_t add_1(uint32_t a) { a += 1; return a; }

You show one example where C doesn't have problems, but that's a much weaker claim than it sounds. "Here's one situation where this here gun won't blow your foot off!" For what it's worth, C++ also passes your test here. You picked an example so simple that it's not very interesting.

'eru implied `a += 1` has undefined behavior; I provided a trivial counter-example. If you'd like longer examples of C code that performs unsigned integer addition then the internet has many on offer.

I'm not claiming that C (or C++) is without problems. I wrote code in them for ~20 years and that was more than enough; there's a reason I use Rust for all my new low-level projects. In this case, writing C without undefined behavior requires lots of third-party static analysis tooling that is unnecessary for Rust (due to being built in to the compiler).

But if you're going to be writing C as "portable assembly", then the competition isn't Rust (or Zig, or Fortran), it's actual assembly. And it's silly to object to C having undefined behavior for signed integer addition, when the alternative is to write your VM loop (or whatever) five or six times in platform-specific assembly.

Re: Performance of the Python 3.14 tail-call interpreter

#39
post #22

Earlier quoted context omitted.

You show one example where C doesn't have problems, but that's a much weaker claim than it sounds. "Here's one situation where this here gun won't blow your foot off!" For what it's worth, C++ also passes your test here. You picked an example so simple that it's not very interesting.

'eru implied `a += 1` has undefined behavior; I provided a trivial counter-example. If you'd like longer examples of C code that performs unsigned integer addition then the internet has many on offer. I'm not claiming that C (or C++) is without problems. I wrote code in them for ~20 years and that was more than enough; there's a reason I use Rust for all my new low-level projects. In this case, writing C without unde…

Forth might be a better competition for 'portable assembly', though.

Re: Performance of the Python 3.14 tail-call interpreter

#40

Earlier quoted context omitted.

When the code has hit the IR in clang or gcc, there is no 'a' (we know that with certainty, since SSA form doesn't mutate but assigns to fresh variables). We don't know if there will be an increment of 1, the additions could be coalesced (or elided if the result can be inferred another way). The number can even decrease, say if things have been handled in chunks of 16, and needs to be adjusted down in the last chunk.…

From a high-level academic view, yes, the compiler is allowed to perform any legal transformation. But in practice C compilers are pretty conservative about what they emit, especially when code is compiled without -march= . You don't have to take my word for it. Go find a moderately complex open-source library written in C, compile it, then open up the result in Hexrays/Ghidra/radare2/whatever. Compare the compiled f…

-O3 does autovectorization: turning your loops into a bunch of SIMD instructions, sometimes even drastically changing performance profile.

If autovectorization is "not that much magic" then idk what else it is.

Post reply on HN