News for hackers. Stuff that matters.
Re:How Many Lines of C It Takes to Execute a and B in Python? by andai on Monday December 11, 2023 @02:35PM (#38604384)
Can someone explain what exactly it is about Python's design that makes it slow?

What changes would have to be made to speed it up? Obviously changing its core design now would break things, but my question is, can we can imagine an alternate universe Python that's as close as possible to our Python, except really fast? What would be different?

Re:How Many Lines of C It Takes to Execute a and B in Python? by AlotOfReading on Monday December 11, 2023 @03:29PM (#38605072)
It's the whole design. Every object creates allocation/gc overhead, bytecode dispatch is a major bottleneck, attribute lookup is expensive, objects are expensive, namespaces are expensive, etc.

You can change things internally (e.g. optimizing opcode parsing), fixed object layouts, restricting mutability, converting everything to predictable array accesses, but you'll likely just end up with something like Lua or Wren rather than Python, and people like Python specifically because of the ecosystem that's built up around that dynamicism over the years.

Re:How Many Lines of C It Takes to Execute a and B in Python? by pjmlp on Tuesday December 12, 2023 @02:28AM (#38610678)
Smalltalk, Common Lisp and SELF are just as dynamic if not more, with a JIT.
Re:How Many Lines of C It Takes to Execute a and B in Python? by ngcc_hk on Thursday December 14, 2023 @05:50PM (#38648488)
That is a question in my mind throughout reading this discussion.

How about objective c vs swift from a dynamic at least type vs static one. Can swift be glue.

Re:How Many Lines of C It Takes to Execute a and B in Python? by whizzter on Wednesday December 13, 2023 @05:33PM (#38635142)
Iirc the slowness of CPython in all the above mentioned are artifacts of the implementation rather than the language itself.

The huge issue is that a big selling point for Python was the easy C-api integration providing lots of useful functionality via libraries now works as a chain that limits how many changes can be made (see any GIL-removal discussion).

The most sane way forward would be to mandate a conversion to a future-proof C-api (PyPy has already designed an initial one iirc that's tested and also has CPython support) that packages would convert to over time.

CPython will probably never go away due to many private users of the old api, but beginning the work towards implementation independancy in the package ecosystem at large could allow _language compatible_ runtimes with V8/JSCore/LuaJIT-like performance for most new projects.

It all depends on the entire community though and that in turn depends on the goodwill of the CPython team to support this.

Re:How Many Lines of C It Takes to Execute a and B in Python? by lifthrasiir on Wednesday December 13, 2023 @11:20PM (#38637738)
I should clarify that LuaJIT is not universally used either, because LuaJIT doesn't support anything after Lua 5.1 among other reasons. People often claim that Lua is a relatively speedy language due to the existence of LuaJIT but it's not entirely correct.
Re:How Many Lines of C It Takes to Execute a and B in Python? by whizzter on Saturday December 16, 2023 @05:51AM (#38663325)
It DIDN'T because Mike Pall(LuaJIT author) was on a hiatus from develoment. He's resumed work since and many 5.2+ features has been incorporated now even if the biggest language-turd (1: _ENV) is explicitly incompatible.

The problem is that _ENV explicitly exposes the lexical environment as regular objects prohibiting optimizations, even JavaScript has _removed_ a similar feature (2: the with statement) when running in "strict" mode to simplify optimizations.

LuaJIT _could_ implement the _ENV blocks but it'd seep into large parts of the codebase as ugly special cases that'd slow down all code in related contexts (thus possibly breaking much performance for code in seemingly unrelated places to where _ENV exists).

To compare from an implementation optimization perspective, exposing _ENV is actually __worse__ than what CPython has with the GIL for example.

Luckily "with"-statements in JS was seldomly used so implementers ignored it's existence as long as it's not used(but still have to consider it in implementations, thus adding more workload), but it's an wart that will kill many optimizations if used.

For most practical purposes most people are fine without "with" or _ENV and the languages are fast enough.

https://luajit.org/extensions.html

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe...

Re:How Many Lines of C It Takes to Execute a and B in Python? by steveklabnik on Monday December 11, 2023 @03:54PM (#38605373)
I am going to answer this question in a roundabout way: first showing examples of different code gen in Rust, because it is more straightforward, but then I will reach for an example with Ruby, because I know it better than Python, but I believe it is similar enough that you will get the gist.

If I write a function like this in Rust:

  pub fn add(x: i32, y: i32) -> i32 {
      x + y 
  }
this will compile to this assembly (on x86_64):

  add:
    leal (%rdi,%rsi), %eax
    retq
two instructions. This is because in Rust, free functions exist, have a name, and they are called by name. There's an additional twist here though too, let's check it out in debug mode, with optimizations off:

  add:
    subq $24, %rsp
    movl %edi, 16(%rsp)
    movl %esi, 20(%rsp)
    addl %esi, %edi
    movl %edi, 12(%rsp)
    seto %al
    testb $1, %al
    jne .LBB0_2
    movl 12(%rsp), %eax
    addq $24, %rsp
    retq

  .LBB0_2:
    leaq str.0(%rip), %rdi
    leaq .L__unnamed_1(%rip), %rdx
    movq core::panicking::panic@GOTPCREL(%rip), %rax
    movl $28, %esi
    callq *%rax
    ud2
There's a few things going on here, but the core of it is that in Rust, in debug mode, overflow of addition is checked, but in release mode, wrapping is okay, and so the compiler can eliminate the error path. This is an example of language semantics dictating particular implementation: if I require overflow checks, I am going to get more code, because I have to perform the check. If I do not require the checks, I get less code, because I do not perform the checks. (Where this gets more interesting is in larger examples where the checks get elided because the compiler can prove they aren't necessary, but this is already a tangent of a tangent.)

In Ruby, there are no free functions. If I write a similar add function:

  def add(x, y)
    x + y
  end
This function is not a free function: it is a new private method on the Object class. When you invoke a function in Ruby, it's not like Rust, where you simply find the function with the name you're invoking, and then call it. You instead perform "method lookup," which has some details I will elide, but for the purposes of this discussion, the idea is that you first look at the receiver to see if it has the add method defined, and then if it does not, you look at the receivers' parent class, and if it's not there, you keep going until you hit the top of the hierarchy. Once the method definition is found, you then invoke it.

Now, it's not as if Rust doesn't also have method lookup (though the algorithm is entirely different), but Rust's design means that method lookup (in the vast majority of cases) is a compile-time thing: the lookup happens while you're building the software, and then at runtime, it simply calls the function that you found.

So why can't Ruby run method lookup at compile time? Well, for one, I left out an important second step: Ruby provides a method called method_missing, as a metaprogramming tool. What this means is, if we look the whole way up the object hierarchy and do not find a method named add, we will then re-traverse the entire ancestor tree again, instead invoking each class's method_missing method on the way. method missing takes the name of the method that was trying to be called, the arguments to it, and any block passed to it, and you can then do stuff to figure out if you want to handle this. This means that, even if no add function is defined, it still may be possible for the call to succeed, thanks to a method_missing handler.

Okay well why can't we do that at compile time? Well, Ruby also lets you redefine functions at runtime at basically any time. The define_method method can be called and generate a method on anything, anywhere you want, for whatever reason. You could do this based on user input, even! And yes, that would be a terrible idea, and you probably shouldn't do it, but the implementation of the language requires at least some sort of runtime computation to pull this off in the general case.

Now, I also want to point out that in my understanding, there's caching on method lookup, so that can help reduce the cost in many scenarios. But the point stands that the language has features that Rust does not, and those features mean that certain things must be more expensive than languages that do not have those features.

> can we can imagine an alternate universe Python that's as close as possible to our Python, except really fast? What would be different?

We could, but you lose compatibility with most Python code, and so you're effectively creating a new language. People do try this though, Mojo being an example of this very recently. I am excited to see how it goes.

Re:How Many Lines of C It Takes to Execute a and B in Python? by andai on Monday December 11, 2023 @04:38PM (#38605867)
Thanks. That's fascinating about Ruby, I'll have to look into that.

I'm not an expert on Python but I don't see how Python is significantly more dynamic than e.g. JavaScript. I think PyPy and JS performance is comparable (or at least within the same order of magnitude), so I think it largely comes down to implementation, i.e. prioritizing performance.

I think if it had been Python (or Ruby for that matter) in the browser instead of JS, it would run about as fast as JS does today.

Re:How Many Lines of C It Takes to Execute a and B in Python? by lmm on Monday December 11, 2023 @05:28PM (#38606342)
> I'm not an expert on Python but I don't see how Python is significantly more dynamic than e.g. JavaScript.

JS has much less in the way of magic methods that can affect "normal" object behaviour, and it doesn't have metaclasses in the way that Python does at all. Most of this customization goes unused most of the time, but the runtime still has to handle it in case it's being used this time.

Re:How Many Lines of C It Takes to Execute a and B in Python? by pjmlp on Tuesday December 12, 2023 @02:31AM (#38610692)
You should have added Ruby JITs into the explanation, with the related generated Assembly code. :)
Re:How Many Lines of C It Takes to Execute a and B in Python? by steveklabnik on Tuesday December 12, 2023 @12:19PM (#38615222)
Yeah that would be fun but I don't have a Ruby environment installed and the comment is already getting very long; if I ever turn this into a blog post someday maybe I will add that :)
Re:How Many Lines of C It Takes to Execute a and B in Python? by igouy on Tuesday December 12, 2023 @06:16PM (#38620180)
You just happened to post a day after "Ruby 3.3.0-Rc1 Released" "many performance improvements especially YJIT"

https://news.ycombinator.com/item?id=38599293