[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)