[HN Gopher] How many lines of C it takes to execute a + b in Python
       ___________________________________________________________________
        
       How many lines of C it takes to execute a + b in Python
        
       Author : taubek
       Score  : 259 points
       Date   : 2023-12-10 12:28 UTC (1 days ago)
        
 (HTM) web link (codeconfessions.substack.com)
 (TXT) w3m dump (codeconfessions.substack.com)
        
       | ctenb wrote:
       | What's the answer?
        
         | a3w wrote:
         | Running `__radd__(tyepof(b) b)` on `a` seems like a complicated
         | problem. So: Many LoC?
         | 
         | Or, the generic, useless but correct, answer: it depends (as
         | the linked article said, too)
        
           | varjag wrote:
           | It shoud've been possible to establish the lower and upper
           | bounds.
        
             | baq wrote:
             | it's about as possible as solving the halting problem if
             | you allow for operator overloading.
        
               | varjag wrote:
               | Leaving aside the apparent confusion between C and C++,
               | do you really imply overloading could make adding two
               | fixed size numbers in Python take unbounded time?
        
               | baq wrote:
               | I'm saying a + b in Python can do whatever you override
               | the __add__/__radd__ to. Not sure why you invoke C/C++
               | here.
               | 
               | If you only limit yourself to numbers (the title doesn't
               | specify that) it should be bounded, but the article goes
               | into some depth here, so I'll leave it at that.
        
               | varjag wrote:
               | Fair!
        
               | FartyMcFarter wrote:
               | No. All you need to do is use a code coverage tool to
               | find out which lines are run.
        
         | andromaton wrote:
         | Article does not answer the question on its title.
        
         | Karellen wrote:
         | It's hard to say. How many lines of code does it take to call
         | typeobj->tp_as_number->nb_add()
         | 
         | when `tp_as_number` is a pointer to a `struct float_as_number`,
         | and `nb_add` is a pointer to `float_add`?
         | 
         | Do struct definitions count as "lines of code _called_ "?
        
       | andai wrote:
       | A while back someone posted their patch to cpython where they
       | replaced the hash function with a fast one and claimed this
       | dramatically sped up the whole Python runtime.
       | 
       | They claimed that the hash function was used constantly --e.g. 11
       | times in print("hello world")--because it's used to look up
       | object properties.
       | 
       | Apparently the default implementation is not optimized for
       | performance but for security, just in case the software is
       | exposed to the web. None of my Python programs are, so assuming
       | all this is true, I'd much prefer to have a "I'm offline, please
       | run twice as fast!" flag (or env variable).
        
         | tyingq wrote:
         | It looks like you can disable the slower randomized hashing
         | yourself by setting PYTHONHASHSEED to 0. Though I don't know if
         | there's further speedup to be had by using a different hash
         | implementation.
         | 
         | https://docs.python.org/3/using/cmdline.html#envvar-PYTHONHA...
         | 
         | The original issue: https://bugs.python.org/issue13703
        
           | masklinn wrote:
           | That only disables the keying of the hash function (sets the
           | initial value to 0), it does not _change_ the hash function.
           | 
           | The hashseed is a per-process value, it has basically no
           | impact on performances.
        
         | a_humean wrote:
         | I'm aware that Rust has something similiar for things like
         | `std::collections::HashMap`. By default:
         | 
         | > The default hashing algorithm is currently SipHash 1-3,
         | though this is subject to change at any point in the future.
         | While its performance is very competitive for medium sized
         | keys, other hashing algorithms will outperform it for small
         | keys such as integers as well as large keys such as long
         | strings, though those algorithms will typically not protect
         | against attacks such as HashDoS.
         | 
         | https://doc.rust-lang.org/std/collections/struct.HashMap.htm...
         | 
         | However, Rust also lets you pick or implement your own hash
         | algorithm if you want to optimise for your usecase.
        
           | eesmith wrote:
           | Python 3.11 appears to have switched to SipHash 1-3 for
           | strings, from 2-4, following the lead of Rust and Ruby.
           | https://github.com/python/cpython/issues/73596
           | 
           | However, Python does not use it for integers;
           | >>> hash(10)       10       >>> hash(100)       100       >>>
           | hash(2**61-2) == 2**61-2       True       >>> hash(2**61-1)
           | 0
        
             | danhau wrote:
             | That's good though, right? Is there a reason for not using
             | an identity hash (is that the right term?) for integers?
        
               | ynik wrote:
               | That depends on the hash table implementation and the
               | distribution of the integers.
               | 
               | For the commonly used hash tables with prime size that
               | use modulo to turn the hash code into a slot index, an
               | identity hash for integers is usually fine (unless many
               | integers are multiples of the prime size).
               | 
               | But other hash tables use power-of-two size to replace
               | the modulo operation with a faster bit-and operation. Now
               | an identity hash for integers is much more problematic,
               | e.g. if all integers are multiples of 1000, only 1/8th of
               | the table slots can be used.
               | 
               | The latter kind of hash tables would like all bits in the
               | hash value to be well-distributed; and this is typically
               | not true of the underlying integers. So an additional
               | mixing operation needs to be used. Whether that mixing
               | happens in the hash function or in the hash table depends
               | on the implementation (for some, it's even configurable,
               | e.g. is_avalanching marker in ankerl::unordered_dense).
        
               | vhcr wrote:
               | Don't use user-supplied integers on dicts or sets on
               | Python:
               | 
               | >>> {i for i in range(10000)}
               | 
               | Takes 0.005s
               | 
               | >>> {i * sys.hash_info.modulus for i in range(10000)}
               | 
               | Takes 0.76s
        
           | yakubin wrote:
           | Which is why the Rust compiler itself uses a non-
           | cryptographic hash, which takes just 3 x86 instructions and
           | can work on 8 bytes at a time: <https://github.com/rust-
           | lang/rustc-hash/blob/master/src/lib....>
        
         | eesmith wrote:
         | Your "just in case the software is exposed to the web" should
         | be "exposed to untrusted data." The very old Python hash could
         | be DoS'ed reading a data file. The original randomized version
         | required some feedback to figure out the hash, so generally
         | required some sort of interaction.
         | 
         | I find that hard to believe that's a performance bottleneck.
         | String hashes are all cached, and names like "print" are
         | interned.
         | 
         | For a 2x overall gain I would expect to see the hash function
         | pop up easily in my profiling, but I haven't seen it in my own
         | profiling which was looking for simple things like that.
         | 
         | When siphash was evaluated, quoting
         | https://peps.python.org/pep-0456/#performance , "In general the
         | PEP 456 code with SipHash24 is about as fast as the old code
         | with FNV" and "The summarized total runtime of the benchmark is
         | within 1% of the runtime of an unmodified Python 3.4 binary".
         | 
         | Since then they switched from siphash24 to the faster
         | siphash13. https://github.com/python/cpython/pull/28752
        
         | ynik wrote:
         | AFAIK Python strings cache their hash value. All the hash table
         | lookups for object properties should be compile-time constant
         | strings that reuse the string object, and thus also reuse the
         | cached hash value. It may take 11 hash computations for the
         | first print("hello world"), but the second call shouldn't take
         | any.
        
         | petters wrote:
         | Do you mean this post?
         | https://www.reddit.com/r/Python/s/raofvsKCiz That speedup was
         | disputed in the comments. I haven't tried it myself though
        
           | masklinn wrote:
           | https://www.reddit.com/r/Python/comments/mgi4op/comment/gswg.
           | ..
           | 
           | I'd use "thoroughly disproven" rather than "disputed".
           | 
           | I'm sure the hash function can be changed, but as various
           | comments noted:
           | 
           | - the benchmark was nonsensical
           | 
           | - cpython caches string hashes, and "symbols" are interned,
           | so outside of dynamic attribute access from dynamically
           | constructed strings each hash for attribute purposes or
           | namespace lookup is computed once per process
           | 
           | - and finally (though probably not the biggest issue) xxhash
           | is known for being mostly useful on larger sizes (>128 bytes,
           | although you can find better hashes (city IIRC) up to 512 or
           | so)
           | 
           | Much like Rust, CPython uses siphash as its default, it's a
           | pretty good all rounder though not the fastest. It actually
           | used to use FNV before HashDOS.
           | 
           | CPython does suffer from the inability of users to configure
           | hash functions since it's an object property rather than a
           | container property.
        
             | tialaramex wrote:
             | Rust is doing something even more subtle here than just
             | making the hash function a container property. There are
             | two inter-related Rust traits, Hash is a trait which types
             | implement to explain abstractly how to hash that type, but
             | it's written in terms of a Hasher trait so you can drop in
             | a different function with the same API. The Rust standard
             | library provides a derive macro for Hash, so most people
             | can just gesture vaguely at their custom type and have it
             | hashed correctly for any hash functions they need. The
             | naive approach here easily ends up doing a bad job either
             | colliding hashes for correlated objects in a surprising way
             | or emitting different hashes for equivalent objects because
             | people who make a user defined type probably aren't hashing
             | experts.
        
               | masklinn wrote:
               | We can get into the weeds about the details, but what I'm
               | talking about is mostly that in Python (and in most
               | languages really, AFAIK Ruby, Java, or C# are the same to
               | list just a few) objects have to return the hash itself,
               | so the hash function is fixed by the type.
               | 
               | In Rust, the Hash trait is only used to feed data to a
               | hasher such that the type can decide _what_ should be
               | hashed. The creation of the hasher is done by the
               | collection, and thus provides better opportunities  /
               | flexibility in customising the hash function.
        
               | tialaramex wrote:
               | The derive macro is also key here. Have you ever looked
               | at the machinery needed for Java's URI type to have a
               | halfway useful hashCode implementation? It's pretty
               | elaborate, there's just no way the average programmer
               | will do that work correctly for their own types even
               | ignoring the desire to allow different hash functions.
               | Rust programmers are almost always able to just write
               | #[derive(Hash)] and not worry about it.
        
         | Asraelite wrote:
         | This may be a somewhat uninformed opinion, but I think CPython
         | is just straight up not particularly good software. There are a
         | million and one optimizations that other major scripting
         | runtimes (V8, LuaJIT, PyPy, Ruby YJit etc.) have had for years
         | that CPython is lacking. This is by design though. CPython has
         | never been focused on performance, that's why it's not even
         | JIT. It optimizes for simplicity and easy interoperability with
         | C.
         | 
         | The problem is that because Python is such a ubiquitous
         | language, CPython gets more attention than it deserves. People
         | see it as an archetypical implementation of a scripting
         | language. We get blogposts like this examining its inner
         | workings, discussions about how its performance could be
         | improved, comparisons of its speed vs. compiled languages, and
         | tutorials on how to optimize code to run faster in it. I feel
         | like all of this effort would be better spent on discussions
         | about runtimes that actually try to be fast.
        
           | lifthrasiir wrote:
           | Python rather _happened to_ have a language design and C API
           | design that doesn 't allow a simple performant
           | implementation. Even PyPy was not that fast compared to other
           | JIT implementations, while its C API support was always
           | subpar. It is easy to say that Python should trade them off
           | for performance, but they are one of the key reasons for
           | Python's success after all.
        
             | whizzter wrote:
             | The semantic issues of making a performant Python language
             | implementation are more or less exactly the same as for JS
             | and Lua, optimizing Ruby seems to possibly have even more
             | "magic" that needs patching but we've seen the Shopify team
             | get cracking on that (it includes MaximeCB that did
             | HiggsJS).
             | 
             | PyPy is in many aspects to be rated as a research project
             | that tried a novel approach to reduce the workload compared
             | to the manhours poured into V8,etc. LuaJIT managed with
             | less with a focused language and a really capable lead.
             | (Also I wouldn't be surprised if the PyPy team has also had
             | to make compromises to get some kind of compatibility)
        
               | lifthrasiir wrote:
               | Unfortunately Python's innard is much more complicated
               | than most expectations. You have named JS and Lua, but
               | those languages never have "magic" methods---JS instead
               | has prototypes and more recently proxies, while Lua has
               | metatables. Ordinary objects aren't magic in this sense,
               | and conversely magical objects are generally deliberate
               | choices in those languages. But Python's magic
               | `__dunder__` methods are everywhere including ordinary
               | objects (and customized objects that look like ordinary
               | objects, e.g. `list` subclasses). That alone complicates
               | a lot of things.
        
               | masklinn wrote:
               | It's not entirely true that JS does not have magic
               | methods. `valueOf` and `toString` can show up
               | surprisingly deep into the resolution of operations, and
               | recent JS has "well known symbols" to implement or
               | override behaviour.
               | 
               | However it is true that this is much, much less extensive
               | than it is in Python. As of 3.12, section 3.3 ("special
               | method names") of the data model documentation lists 107
               | entries (although some of them only apply to class
               | protocols, and a handful are duplicates for async
               | versions / context of sone operations).
        
               | lifthrasiir wrote:
               | `valueOf` and `toString` are indeed fairly complex (and
               | it's fun to consider when both are implemented ;-), but
               | less of concern for tracing JIT engines because you can
               | have efficient type-specialized implementations for most
               | cases. Type specialization in Python is not that huge
               | win...
        
               | Sesse__ wrote:
               | Also, you have craziness like quite regular iteration
               | being implemented using exceptions, which are not exactly
               | trivial to optimize.
        
               | jerf wrote:
               | In the late 1990s and early 200xs, there were a lot of
               | claims that there was no such thing as a "slow language",
               | that all languages can be run as quickly as C if you just
               | built a sufficiently smart compiler and/or runtime.
               | 
               | I haven't heard anyone make this claim in a while. The
               | inability to speed up Python beyond a certain point
               | despite a lot of clever approaches taken was probably a
               | good chunk of the reason, the remainder being the wall
               | that JS has hit despite the huge effort poured into it
               | where it is still quite distinctly slower than C.
               | 
               | If I were designing a language to be slow, but not like
               | stupidly slow just to qualify as an esolang, but where
               | the slowness still contributed to things I could call
               | "features" with a straight face, it would be hard to beat
               | Python. I suppose I could try to mix in more of a TCL-
               | style "everything is a string" and "accidentally" build
               | some features on it that bust the string caching, but
               | that's about all I can think of at this point.
        
               | esafak wrote:
               | "Tech debt doesn't matter!"
               | 
               | "We're going to redesign it the right way after we get
               | this version out the door!"
        
               | wongarsu wrote:
               | Arguably those redesigns might have happened if the
               | Python 2->3 transition hadn't received a decade of
               | extremely vocal pushback
        
               | fullspectrumdev wrote:
               | What's funny is the breaking change that caused us the
               | most headaches with 2->3 was the changes to fucking
               | print.
               | 
               | I'm still finding broken print as a statement instead a
               | function issues in codebases, somehow.
        
               | andai wrote:
               | 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?
        
               | AlotOfReading wrote:
               | 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.
        
               | steveklabnik wrote:
               | 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.
        
               | andai wrote:
               | 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.
        
               | lmm wrote:
               | > 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.
        
               | slaymaker1907 wrote:
               | You could add in continuation support as well as
               | unbounded stack size to make it even more difficult to
               | implement efficiently. There are tricks these days for
               | implementing continuations somewhat efficiently (and by
               | somewhat, I mean that they're only 3-5x slower than
               | explicit continuation passing with lambdas), but these
               | tricks largely don't work in WASM without doing something
               | extreme like completely ignoring the WASM stack and
               | storing return addresses/current continuation on the WASM
               | heap.
               | 
               | Unbounded stack size is similarly difficult for WASM
               | because like before, you have to be very careful about
               | using the WASM stack.
               | 
               | Even with C++, you basically need to drop down to
               | intrinsics or assembly to make full use of SIMD.
        
               | steveklabnik wrote:
               | I think this is really complicated. I do believe that it
               | is important to separate language from implementation,
               | and it is also true that different implementations can
               | have different performance profiles. It is also true that
               | the semantics of the language can effectively require
               | specific implementation details that can affect
               | performance either way. So there's always gonna be bounds
               | to any particular languages' ability to be faster.
               | 
               | That is why the "as fast as C with the sufficiently smart
               | compiler" never truly came to pass in a general sense,
               | even if many languages that were slow to start have
               | gotten way faster with better implementations.
        
               | spacechild1 wrote:
               | Lua has metamethods:
               | https://www.lua.org/manual/5.3/manual.html#2.4
        
               | KMag wrote:
               | > LuaJIT managed with less with a focused language and a
               | really capable lead.
               | 
               | It's pretty well established that "Mike Pall" is the pen
               | name for an AI sent from the future for unknown reasons.
               | It disappeared from our light cone due to a rift in
               | causality, presumably because it succeeded in whatever
               | changes it wanted to make in the future.
        
           | sp332 wrote:
           | The last couple of CPython versions have had dramatic speed
           | improvements, so that demonstrates your point but also gives
           | some hope that things are changing on that front.
        
             | coldtea wrote:
             | Dramatic? Barely. Even less than half of the non-dramatic
             | speed improvements they promised. The promise was like "5
             | times faster in the next 4 releases", and the improvements
             | in 2 releases thus far was like 20% at best (so not even 2x
             | faster).
             | 
             | Compared to JS pre-and-after modern engines it's a tiny
             | improvement.
        
           | mseepgood wrote:
           | If this is by design and it optimizes for simplicity and easy
           | interoperability with C, why would you assess its quality as
           | 'good software' based on criteria that are non-goals?
        
             | Asraelite wrote:
             | In my eyes, "good software" does not necessarily just mean
             | something that accomplishes its goals. You can set a goal
             | of being bad, and that's kind of what CPython is doing.
             | 
             | Of course, "good" and "bad" are relative. If you don't care
             | about performance then there's nothing wrong with CPython.
        
               | acqq wrote:
               | I agree with that estimate, I think it was really the
               | idea of Python to be less than "good" by design.
               | 
               | I mean one doesn't need more than:                   >>>
               | exit         Use exit() or Ctrl-D (i.e. EOF) to exit
               | 
               | to see. They have that special handling, but they still
               | don't want to let you out, because... IMO, they just
               | _want_ to be annoying.
               | 
               | When a solution could be something like:
               | Note: exit() is needed in scripts. In this prompt Ctrl-D
               | (i.e. EOF) can also be used.         Exiting.
        
               | heyodai wrote:
               | Yeah, that message is a pet peeve of mine. Like, you know
               | what I'm trying to do.
        
               | Smaug123 wrote:
               | It sort of doesn't! It's just doing exactly the same
               | thing it would do with any other identifier: it's
               | printing its `repr`. Try `repr(exit)`!
        
               | a1369209993 wrote:
               | More to the point, try `[len,exit,repr]`. Consider what
               | would happen if `exit.__repr__()` _did_ terminate the
               | program.
        
               | monoprotic wrote:
               | There has been discussion of this change a few times,
               | e.g. https://bugs.python.org/issue44603
               | 
               | I generally agree with your overall sentiment, but I
               | think it's important to note that the behavior is _not_
               | special handling; it's just the normal `repr` behavior at
               | the REPL, where `exit` is an object like any other, and
               | `repr(exit)` is that message.
        
               | zitterbewegung wrote:
               | I think this is to be consistent that exit is not a
               | statement but a function. This was done with print. 3.0
               | was an ergonomic fix for cpython developers and language
               | consistency fixes (also redoing the Unicode
               | implementation).
        
           | zitterbewegung wrote:
           | I don't disagree with you but, now you can retrofit a JIT
           | into python. https://blog.pyston.org/2022/09/29/announcing-3-
           | 7-3-10-suppo...
        
             | acqq wrote:
             | "We think of the breakdown roughly as follows: of our
             | roughly 30% original speedup, 10% is going into Pyston-
             | lite, 10% was done independently by the CPython team
             | between 3.8 and main, and the remaining 10% we are hoping
             | to contribute back upstream."
             | 
             | To compare with Javascript: if I remember, as it appeared,
             | V8 JIT was orders of magnitude faster, compared to the
             | interpreted code.
        
               | zitterbewegung wrote:
               | There was also a bunch of effort being put into
               | efficiency of JavaScript. But now the tables have been
               | changed and Microsoft is putting a large amount of effort
               | into Python .
        
           | heavyset_go wrote:
           | Recent CPython development has been towards optimizations and
           | addressing use cases that benefit from optimizations, some
           | coming from the faster CPython initiative. You might just get
           | your JIT[1].
           | 
           | At the same time, I also agree with your sentiment.
           | 
           | [1] https://github.com/faster-cpython/ideas/wiki/Workflow-
           | for-3....
        
           | nostrademons wrote:
           | It's great software _for the use case that Python is intended
           | for_. Python is supposed to be glue code. You embed a
           | scripting runtime in your application, do all the heavy
           | lifting in C, but configure your building blocks in Python so
           | that you can easily _reconfigure_ them as needs change.
           | 
           | NumPy, SciPy, TensorFlow, PyTorch, JAX, Pandas, Pillow, lxml,
           | cjson, PyCapnP, Tornado, fast-avro, etc. all get it right.
           | They are wrappers around C (or in some cases:
           | Fortran/assembly/CUDA) code, where the overflow of Python
           | method dispatch is dwarfed by the hundreds of thousands of
           | iterations of an inner loop that's in optimized, vectorized
           | assembly. Django, Protobufs, and Avro get it wrong (often for
           | portability or developer velocity sake), where they wrote the
           | whole library in Python at the expense of performance.
           | 
           | I was briefly tempted to write an API-compatible
           | reimplementation of Django with the core in C++ when I left
           | Google, but by then Django (and server-side web programming)
           | was already falling out of favor, and if you're just shipping
           | JSON to a SPA you can use cjson with any number of fast wsgi
           | or asgi gateways.
        
             | bb88 wrote:
             | I've been playing around with HTMX with Django, and it
             | seems simple enough for server side rendering. It works
             | well with the Django templating system.
             | 
             | I feel like as an industry we should step back and take a
             | serious look at front end frameworks from first principles.
             | One aspect that's clear to me, is that we should make
             | modifications to HTML to support HTMX like transactions.
        
             | oivey wrote:
             | Arguably no one's goal is to glue pieces of C together.
             | That's too abstract. Their goal is to do something like
             | write a performant web server, maybe easily and quickly.
             | Python's approach is one way to do that, but there are
             | other ways that might be better. Having to write code in
             | two languages to solve a problem, one of which that is
             | difficult to write and has lots of footguns, has some clear
             | downsides.
        
               | nerdponx wrote:
               | Ritchie and Thompson might disagree with you.
        
               | eesmith wrote:
               | Gluing pieces of C together was literally the reason I
               | started using Python back in the 1990s.
               | 
               | My other two main options were Tcl and Perl. Tcl was
               | excellent at gluing, but worse at scaling, with no
               | namespaces (then) and OO only as third-party add-ons.
               | 
               | Perl extensions were not so easy (better with Perl 5),
               | and much as I enjoyed the language, handling complex data
               | structures, was not for the faint-hearted.
               | 
               | Gluing pieces of C together is why we have NumPy, PyQt,
               | pywin32, wxPython, and tens of thousands of other
               | packages that work with C/C++/Fortran libraries.
        
               | nostrademons wrote:
               | Basically nobody's goal is to "write a performant web
               | server" either, it's to _serve data to customers quickly
               | and efficiently_. And that highlights why it may not be
               | worth optimizing the Python web ecosystem. There are so
               | many newer alternatives for that overall goal - Firebase,
               | Amazon Lambda, ditching webapps for native mobile, etc -
               | that it may not make sense to try to optimize an
               | application server unless you work for Google or Amazon,
               | because very few people setup a standalone webserver on
               | bare-metal hardware anymore, and those that do probably
               | aren 't going to try a new and untested alternative.
        
               | kazinator wrote:
               | Nobody's goal is to _serve data to customers quickly and
               | efficiently_ either. It 's more like, _get this startup
               | acquired and buy a ranch in New Zealand_.
        
             | kazinator wrote:
             | > _for the use case that Python is intended for_
             | 
             | Where is this single use case intent articulated, by whom,
             | and what year was it? What is the use case?
             | 
             | Today, it seems that Python is pitched for almost
             | everything, short of ethernet drivers.
        
               | MR4D wrote:
               | From https://www.python.org/about/ (mouse over the
               | "About" menu) :
               | 
               | 'Python is a programming language that lets you work more
               | quickly and integrate your systems more effectively.'
        
               | spookie wrote:
               | This is one of the biggest misunderstandings in computing
               | for the last decade, I feel.
        
               | kazinator wrote:
               | [delayed]
        
               | jasode wrote:
               | _> Today, it seems that Python is pitched for almost
               | everything, short of ethernet drivers._
               | 
               | I think the _" Python is pitched for almost everything,"_
               | in that sentence shows a misinterpretation of gp's
               | phrasing of "use case".
               | 
               | The "use case" isn't about _different subject matter
               | domains_ as if it was a claim about using it as a
               | universal language for writing database kernels or AAA
               | games.
               | 
               | Instead, the "use case" is about the _2-level 2-language
               | architecture_ of (1) a high-level scripting language and
               | (2) extension modules that can be written in low-level C
               | and imported into the interpreter. That 's the "glue
               | language" + "C Language" -- combine the strengths of each
               | language approach. (In contrast, Julia took approach of
               | designing a language that was "fast enough" to avoid the
               | "2 languages issue".)
               | 
               |  _> Where is this single use case intent articulated, by
               | whom, and what year was it? What is the use case?_
               | 
               | The Python "ergonomics use case" (not "domains use case")
               | was originated by Python's inventor Guido van Rossum from
               | the beginning in 1991. A clone of the first Python source
               | code ~1991 has Guido's commentary for importing C
               | modules:
               | 
               | https://github.com/smontanaro/python-0.9.1
               | 
               | Modern libraries like TensorFlow and Pytorch continue the
               | use case of "high-level script glue code calling low-
               | level C code" that was there in 1991.
        
             | tesdinger wrote:
             | > I was briefly tempted to write an API-compatible
             | reimplementation of Django with the core in C++ when I left
             | Google, but by then Django (and server-side web
             | programming) was already falling out of favor, and if
             | you're just shipping JSON to a SPA you can use cjson with
             | any number of fast wsgi or asgi gateways.
             | 
             | I like Django. I need to process data on the server side
             | and like to write that in python because it is more
             | convenient than C. I also built my GUI in Django without
             | knowing JavaScript. "just shipping JSON" seems like a
             | different use case.
             | 
             | I have a piece of hardware (a laboratory hardware switch)
             | that exposes a REST API for CRUD. I wanted to build a GUI
             | that formats and summarizes information and offers
             | convenient control. The data is small enough so that python
             | can process it without becoming the bottleneck. I used
             | Django ORM to model the data and django forms with htmx for
             | the GUI. Authentication was easily added to Django.
             | 
             | The ORM part was a bit painful as Django forms expect a
             | queryset and a queryset is not be the result of a raw sql
             | query. There is a way to feed list of tuples into a choices
             | argument but I decided against that , and instead dumbed
             | down my query so I was able to write it as an django ORM
             | language query.
        
               | tesdinger wrote:
               | This is the problem with SQL queries and Django Forms I
               | am talking about
               | 
               | https://stackoverflow.com/questions/17330158/django-how-
               | to-u...
        
           | matheusmoreira wrote:
           | Just yesterday I compared the small language I created to
           | Python. Was surprised my tree walking interpreter somehow
           | beat Python's bytecode virtual machine at recursive
           | Fibonacci. It was just a simple hyperfine benchmark so I
           | might be confounding code execution performance with the
           | initialization time. Still caught me totally off guard and
           | gave me a huge confidence and motivation boost. I was
           | thinking something like "OK let's see just how bad this thing
           | is" but it actually beat Python at _something_.
        
         | explaininjs wrote:
         | What's more, you can get a significant speedup from your Python
         | scripts by replacing the inbuilt cpython malloc calls with a
         | static "allocate big chunk of stack at the beginning and I'll
         | manage it myself" implementation, falling back to malloc as
         | needed if it grows beyond that. A college class in perf
         | engineering I TA'd did this, the results even a beginner could
         | achieve were compelling, the top of the class produced results
         | quite remarkable indeed..
         | 
         | This is most effective for reducing startup time of short lived
         | scripts, where the runtime is dominated by _many thousands_ of
         | trivial mallocs right at startup. But in general if you can
         | establish a bound on memory, it will be faster to allocate it
         | in one shot.
        
         | 2OEH8eoCRo0 wrote:
         | > I'd much prefer to have a "I'm offline, please run twice as
         | fast!"
         | 
         | If I know anything about programmers, it's that everyone would
         | just use the "go faster" flag by default.
        
         | 60secs wrote:
         | So your solution to if statements is to add more if statements?
        
         | matheusmoreira wrote:
         | Hash function security is certainly a concern due to hash
         | flooding attacks which force worst case performance hash table
         | lookup and leads to denial of service.
         | 
         | https://peps.python.org/pep-0456/
        
       | rhabarba wrote:
       | Sounds like it would be easier to just use C anyway.
        
         | benj111 wrote:
         | Easier for whom?
         | 
         | The whole point of high level languages is that you put in
         | effort upfront to make everyone elses job easier.
         | 
         | By you logic, using machine code directly onto toggle switches
         | is easiest. No assembler to write, no test editor to write.....
        
         | klysm wrote:
         | Easier is exactly what that wouldn't be
        
         | globular-toast wrote:
         | It's even easier to just buy a desk calculator or even just
         | learn long addition and do it with pencil and paper.
        
           | rhabarba wrote:
           | Actually, I generally recommend to not leave easy tasks to
           | the computer. The brain likes to have to do stuff sometimes.
        
             | llamaInSouth wrote:
             | why waste time?
        
         | unnah wrote:
         | Yes, but C is a much more arcane and complicated language. Case
         | in point: instead of writing a+b in Python, in C you would have
         | to write a+b
         | 
         | (Jokes aside, it would really be more complicated in C if a and
         | b were actually strings or lists.)
        
           | Marazan wrote:
           | Or numbers that add up to over the size limit of what ever
           | byte/int/long type you are using.
           | 
           | There's a surprising amount of depth to adding two numbers.
        
           | rhabarba wrote:
           | If a and b are strings, Python would make "ab".
        
           | fractalb wrote:
           | > it would really be more complicated in C if a and b were
           | actually strings or lists
           | 
           | You don't event need strings or lists for that. Just imagine
           | bigger numbers for a and b. Arbitrarily long integer addition
           | is not a native language feature in C.
           | 
           | edit: formatting
        
             | 082349872349872 wrote:
             | to be concrete:                   fac = lambda n: 1 if n<=1
             | else n*fac(n-1)         a, b = fac(42), fac(69)         a +
             | b
             | 
             | is 3 lines of python; how many lines of C code would it
             | take to execute?
        
               | tgv wrote:
               | One: return 1.7112245243e98
               | 
               | Joking aside: these are not catch-all comparisons. Nor is
               | the article. But Python is mucher slower and much safer
               | than C. It's easier to start in Python than in C.
        
               | 082349872349872 wrote:
               | ;-P Just to be pedantic, if you're going to all the
               | trouble to give (modulo lack of a repl) a wrong answer
               | fast, might as well do it _really_ quickly:
               | return 0;
               | 
               | in the hopes that compiles down to something like:
               | xor rax, rax         ret
        
           | benj111 wrote:
           | It still isn't simple
           | 
           | #! /Usr/bin/python
           | 
           | Print(a+b)
           | 
           | V
           | 
           | #include <stdio.h>
           | 
           | Int main (){ Printf("%i", a+b); Return 0; }
           | 
           | And printf is basically a DSL, so it still isn't 'simple' And
           | this is assuming a+b fits into an integer
        
             | jefftk wrote:
             | If you're being silly you don't need a lot of that:
             | $ cat tmp.c         main() {            printf("%d\n",
             | 3+4);         }         $ gcc -w -o tmp.out tmp.c &&
             | ./tmp.out         7
             | 
             | You can even do away with types entirely, as long as you're
             | working with ints:                   $ cat tmp.c
             | foo(x) {            return x+5;         }         bar() {
             | return 4;         }         main() {
             | printf("%d\n", foo(bar()));         }         $ gcc -w -o
             | tmp.out tmp.c && ./tmp.out         9
             | 
             | Rarely a good idea, though!
        
               | benj111 wrote:
               | True, but then someone will pop up and announce UB! (This
               | probably isn't ub though).
               | 
               | Anyway, my main point was that c has more boilerplate.
               | It's never 'a+b'.
               | 
               | Second, printf is complicated, that's aimed more at the
               | op though.
        
           | __loam wrote:
           | C++ otoh is actually more arcane and complicated.
        
           | commandlinefan wrote:
           | > much more arcane and complicated
           | 
           | How much more, though? The conventional wisdom here seems to
           | be that it's worth taking the unavoidable performance hit of
           | dynamically typed scripting languages because the
           | productivity boost to programmers balances it out... but I
           | don't believe I've seen that productivity boost measured.
           | Once you know what you're doing in C, you can do that same
           | things you can do in Python. There's some (fascinating)
           | syntactic sugar in there, but Python can easily be just as
           | incomprehensible as C.
        
             | someNameIG wrote:
             | You can become productive in python faster than you can in
             | C. My background is the natural sciences and on reason
             | python is used a lot in these fields is it's ease of use
             | for people not coming from a computer science/engineering
             | background.
        
         | pjmlp wrote:
         | There are plenty of AOT compiled languages, C isn't the only
         | option, thankfully.
        
         | DarkNova6 wrote:
         | Tell that your average data Joe and he will say "But C is a
         | letter, not a programming language".
        
       | Dwedit wrote:
       | Lines of C aren't the defining factor. You want total
       | instructions executed.
        
         | flohofwoe wrote:
         | IME I'd say a line of typical C code (not necessarily C++ code)
         | maps to around 1..5 instructions on average. It's still a quite
         | useful measure to get a rough idea how much CPU work happens.
        
         | jasode wrote:
         | _> Lines of C aren't the defining factor. You want total
         | instructions executed._
         | 
         | I understand what your clarification is trying to provide but
         | it isn't relevant to this particular thread's article. The
         | article is not about "performance benchmarks" where you need
         | cpu instructions as a definitive unit-of-measure for
         | comparisons.
         | 
         | Instead of measuring performance, the author's theme in this
         | case is more akin to "decompiling" or "reverse-engineering". He
         | takes a tiny piece of Python code and then _maps it back to the
         | actual CPython_ source *.c and *.h files that implements the
         | Python vm. He added several _deep links to the relevant
         | sections of CPython source code on Github_ to help illustrate
         | the mappings between Python 's BINARY_OP to the .c and .h
         | files. The article is sharing the type of knowledge you'd gain
         | by loading up CPython in a debugger and single-stepping through
         | the source code line-by-line.
         | 
         | In other words, the article's title could also have been: _"
         | Which Lines of CPython does it Take to Execute a + b in
         | Python?"_
         | 
         | For the scope of this particular article, the "lines of C"
         | _are_ the defining factor because the subject of dissection is
         | CPython's .c/.h files.
        
         | bufo wrote:
         | It's about 100 for x86_64
         | https://www.computerenhance.com/p/waste
        
       | mobiuscog wrote:
       | There was some high-level discussion of this in the chat Guido
       | van Rossum had with Lex Fridman (timestamped):
       | https://youtu.be/-DVyjdw4t9I?t=3964
        
       | Uptrenda wrote:
       | Python's math operations are going to need a lot of C code
       | because the numbers can be any size. It's part of what makes it
       | so great for scientific computing (as you don't have to spend
       | hours implementing arbitrary precision math - probably badly.) If
       | that's too slow though there's always NumPy.
        
         | KeplerBoy wrote:
         | That's only true for integers.
         | 
         | For scientific applications you'd typically want floating
         | points and Python's floats are just regular ieee-754 doubles
         | (or whatever "double" meant to the compiler used to compile
         | that python interpreter).
        
           | yeshman wrote:
           | Python also has a fixed point arithmetic in its std library:
           | https://docs.python.org/3/library/decimal.html
        
         | cozzyd wrote:
         | If only there were libraries for multiprecison arithmetic in
         | other languages...
        
       | mgl wrote:
       | A more thorough comparison of time, energy and memory required to
       | execute similar Python, C, Rust, Java, C#, etc. code:
       | 
       | https://stratoflow.com/efficient-and-environment-friendly-pr...
       | 
       | And why Mojo could be an answer for high (well, higher)
       | performance Python: https://stratoflow.com/introduction-to-mojo-
       | programming-lang...
        
       | geertj wrote:
       | The Python C API has become so verbose that I recommend against
       | using it directly. Recently I mapped a few external libraries
       | using Nanobind, and the result was significantly more concise.
       | Nanobind uses the type system of modern C++ (17 and above) to
       | provide most argument conversions, Mapping C functions is
       | typically just a few lines of code. It also provides a great way
       | to map C++ constructs such as classes, exceptions, and standard
       | containers.
       | 
       | Nanobind is by the same author that started pybind11, used by
       | Tensorflow and PyTorch. The web site [1] contains a bit more of
       | the rationale.
       | 
       | [1] https://nanobind.readthedocs.io/en/latest/why.html
        
       | BD103 wrote:
       | This was quite interesting, but I'm disappointed it didn't
       | mention how many lines in C it actually took to run. Perhaps a
       | profiler might help calculate this?
        
         | whizzter wrote:
         | If you read the article you can see that some codepaths can
         | invoke Malloc with all the follow-on effects like Kernel
         | boundary crossings that this implies, it's thus quite random.
        
           | serial_dev wrote:
           | It would still make sense to give a number or a range.
        
           | layer8 wrote:
           | Since malloc() is a standard C library function, it would be
           | okay to not count its implementation (which isn't necessarily
           | written in C).
        
       | vitiral wrote:
       | It seems like it doesn't answer it's own question, so I'll pose
       | another.
       | 
       | The entire Lua core is 15kLoC. Is that more than or less than
       | what's needed for python's "a + b", assuming a and b are defined.
       | 
       | I'm genuinely curious.
        
         | kevindamm wrote:
         | Yes, it's a very tootsie-roll-center kind of answer, but it's
         | clearly more than a few.
         | 
         | To answer your question, 15kLoC is more than enough to
         | implement dynamic dispatch and the PyObject base struct, along
         | with the special method logic for __add__ on any python object
         | type.. but still a lot less than what's needed for all the
         | special method types and a lot of boilerplate for the
         | C-compatible interface around those methods.
        
           | demondemidi wrote:
           | I wonder what percent of this audience got the tootsie-roll
           | reference. Does anyone under 40 know it?
        
             | steveklabnik wrote:
             | I am about to turn 38 and I got it, so it's at least
             | _slightly_ lower than 40.
        
       | KolenCh wrote:
       | I have a real life example in this commit:
       | https://github.com/hpc4cmb/toast/pull/380/commits/a38d1d6dbc...
       | 
       | Replacing 2 lines of python code (with tens of glue code in
       | Numba) with hundreds lines of C++ with glue code.
        
       | fweimer wrote:
       | The general topic is covered in Brett Cannon's "Python is
       | (mostly) made of syntactic sugar" series of posts, via
       | translation of high-level semantics into a subset of Python. LWN
       | has a high-level summary: https://lwn.net/Articles/942767/
       | 
       | The actual posts are here: https://snarky.ca/tag/syntactic-sugar/
       | (multiple pages!)
        
       | pphysch wrote:
       | We know that addition is a _relatively_ trivial operation, say,
       | on 64 bit integers.
       | 
       | A more interesting example IMO would be something like "how many
       | lines of C it takes to execute person.name='Bob' in Python, where
       | person.name is undefined".
       | 
       | That would better demonstrate why we use Python in the first
       | place (hint: it's not "to add integers"), while also indicating
       | why it is slow.
        
       | sroussey wrote:
       | You would be shocked at how bad/slow addition is in a CPU/GPU or
       | any digital ALU.
        
       | neillyons wrote:
       | There is a really good ten hour walkthrough of Python 2 internals
       | https://youtube.com/playlist?list=PLzV58Zm8FuBL6OAv1Yu6AwXZr...
        
       ___________________________________________________________________
       (page generated 2023-12-11 23:00 UTC)