I'm a bit confused about the premise. This is not comparing pure Python code against some native (C or Rust) code. It's comparing one Python wrapper around native code (Python's file read method) against another Python wrapper around some native code (OpenDAL). OK it's still interesting that there's a difference in performance, but it's very odd to describe it as "slower than Python". Did they expect that the Python standard library is all written in pure Python? On the contrary, I would expect the implementations of functions in Python's standard library to be native and, individually, highly optimised.
I'm not surprised the conclusion had something to do with the way that native code works. Admittedly I was surprised at the specific answer - still a very interesting article despite the confusing start.
Edit: The conclusion also took me a couple of attempts to parse. There's a heading "C is slower than Python with specified offset". To me, as a native English speaker, this reads as "C is slower (than Python) with specified offset" i.e. it sounds like they took the C code, specified the same offset as Python, and then it's still slower than Python. But it's the opposite: once the offset from Python was also specified in the C code, the C code was then faster. Still very interesting once I got what they were saying though.
It's surprising that something as simple as reading a file is slower in the Rust standard library as the Python standard library. Even knowing that a Python standard library call like this is written in C, you'd still expect the Rust standard library call to be of a similar speed; so you'd expect either that you're using it wrong, or that the Rust standard library has some weird behavior.
In this case, it turns out that neither were the case; there's just a weird hardware performance cliff based on the exact alignment of an allocation on particular hardware.
So, yeah, I'd expect a filesystem read to be pretty well optimized in Python, but I'd expect the same in Rust, so it's surprising that the latter was so much slower, and especially surprising that it turned out to be hardware and allocator dependent.
It's just the spin of it that threw me off. You're right: "why is a C implementation so much faster than a widely used Rust implementation" is a valid and interesting question. But phrasing it as "why is a Python function faster than a Rust function", when it's clearly not the comparison at all, distracts from the real question.
I don't understand why Python gets shit for being a slow language when it's slow but no credit for being fast when it's fast just because "it's not really Python".
If I write Python and my code is fast, to me that sounds like Python is fast, I couldn't care less whether it's because the implementation is in another language or for some other reason.
Because for any nontrivial case you would expect python+compiled library and associated marshaling of data to be slower than that library in its native implementation without any inyerop/marshaling required.
When you see an interpreted language faster than a compiled one, it's worth looking at why, because most the time it's because there's some hidden issue causing the other to be slow (which could just be a different and much worse implementation).
Put another way, you can do a lot to make a Honda Civic very fast, but when you hear one goes up against a Ferrari and wins your first thoughts should be about what the test was, how the Civic was modified, and if the Ferrari had problems or the test wasn't to its strengths at all. If you just think "yeah, I love Civics, that's awesome" then you're not thinking critically enough about it.
Yep, and the next logical question when both implementations are for the most part bare metal (compiled and low-level), is why is there a large difference? Is it a matter of implementation/algorithm, inefficiency, or a bug somewhere? In this case, that search turned up a hardware issue that should be addressed, which is why it's so useful to examine these things.
> Because for any nontrivial case you would expect python+compiled library and associated marshaling of data to be slower than that library in its native implementation without any inyerop/marshaling required.
> When you see an interpreted language faster than a compiled one, it's worth looking at why, because most the time it's because there's some hidden issue causing the other to be slow (which could just be a different and much worse implementation).
On the contrary, the compiled languages tend to only be faster in trivial benchmarks. In real-world systems the Python-based systems tends to be faster because they haven't had to spend so long twiddling which integers they're using and debugging crashes and memory leaks, and got to spend more time on the problem.
I don't doubt that can happen, but I'm also highly doubtful that it's the norm for large, established, mature projects with lots of attention, such as popular libraries and the standard library of popular languages. As time spent on the project increases, I suspect that any gain an interpreted language has over an (efficient) compiled one not only gets smaller, but eventually reverses in most cases.
So, like in most things, the details can sometimes matter quite a bit.
> I don't doubt that can happen, but I'm also highly doubtful that it's the norm for large, established, mature projects with lots of attention, such as popular libraries and the standard library of popular languages.
Code that has lots of attention is different, certainly, but it's also the exception rather than the rule; the last figure I saw was that 90% of code is internal business applications that are never even made publicly available in any form, much less subject to outside code review or contributions.
> As time spent on the project increases, I suspect that any gain an interpreted language has over an (efficient) compiled one not only gets smaller, but eventually reverses in most cases.
In terms of the limit of an efficient implementation (which certainly something like Python is nowhere near), I've seen it argued both ways; with something like K the argument is that a tiny interpreter that sits in L1 and takes its instructions in a very compact form ends up saving you more memory bandwidth (compared to what you'd have to compile those tiny interpreter instructions into if you wanted them to execute "directly") than it costs.
I think there's something to the idea of keeping the program in the instruction cache by deliberately executing parts of it via interpreted bytecode. There should be an optimum around zero instruction cache misses, either from keeping everything resident, or from deliberately paging instructions in and out as control flow in the program changes which parts are live.
There are complicated tradeoffs between code specialisation and size. Translating some back and forth between machine code and bytecode adds another dimension to that.
I fear it's either the domain of extremely specialised handwritten code - luajit's interpreter is the canonical example - of the the sufficiently smart compiler. In this case a very smart compiler.
> On the contrary, the compiled languages tend to only be faster in trivial benchmarks. In real-world systems the Python-based systems tends to be faster because they haven't had to spend so long twiddling which integers they're using and debugging crashes and memory leaks, and got to spend more time on the problem.
This is an interesting premise.
Python in particular gets an absolute kicking for being slow. Hence all the libraries written in C or C++ then wrapped in a python interface. Also why "python was faster than rust at anything" is headline worthy.
I note your claim is that python systems in general tend to be faster (outside of trivial benchmarks, whatever the scope of that is). Can you cite any single example where this is the case?
> Can you cite any single example where this is the case?
Plenty of line-of-business systems I've seen, but systems big enough to matter tend not to be public. Bitbucket's cloud and on-prem version are the only case I can think of where you can directly compare something substantial between an implementation known to be written in Python and an implementation that's known to be written in C/C++ (and even then I'm not 100% that that's what they use).
I wonder if its because we're sometimes talking cross purposes.
For me, coding is almost exclusively using python libraries like numpy to call out to other languages like c or FORTRAN. It feels silly to say I'm not coding in Python to me.
On the other hand, if you're writing those libraries, coding to you is mostly writing FORTRAN and c optimizations. It probably feels silly to say you're coding in Python just because that's where your code is called from.
There is a version of BASIC, a QuickBasic clone called Qb64 that is lightning fast because it transpiles to C++. By your admission a programmer should think that BASIC is fast because he only does BASIC and does not care about the environment details?
It's actually the opposite, a Python programmer should know how to offload most, or use the libraries that do so, out of Python into C. He should not be oblivious to the fact that any decent Python performance is due to shrinking down the ratio of actual Python instructions vs native instructions.
I think maybe it's just semantics as long as everyone agrees where the speedup is happening (at the low level language calls).
I noticed that you're pretty hard in the "basic isn't fast, the thing it transpiles to is fast" camp, but still accidentally said "there is a version of BASIC [...] that is lightning fast" which I'm not sure you think? Highlights just how tricky it is to talk about where speed lives
There is clear distinction between original language design (an interpreter) and a project aiming to recreate a sub-standard of that language and support its legacy codebase via a transpiler.
But you will care if that "python" breaks - you get to drop down to C/C++ and debugging native code. Likewise for adding features or understanding the implementation. Not to mention having to deal with native build tooling and platform specific stuff.
It's completely fair to say that's not python because it isn't - any language out there can FFI to C and it has the same problems mentioned above.
It's pretty hard to draw this line in Python because all built-in types and functions are effectively C extensions, just compiled directly into the interpreter.
Conversely, you can have pure C code just using PyObjects (this is effectively what Cython does), with the Python bytecode interpreter completely out of the picture. But the perf improvement is nowhere near what people naively expect from compiled code, usually.
Yes, which is why I would argue that IO is a particularly bad benchmark here, since everything is just a thin layer on top of the actual syscall, and those layers don't do any real work worth comparing.
The only thing that makes sense to compare when talking about pythons performance is how many instructions it needs to compute something, versus the instructions needed to compute the same thing in C. Those are probably a few orders of magnitude apart.
Usually, yes, but when it's a bug in the hardware, it's not really that Python is fast, more like that CPython developers were lucky enough to not have the bug.
The PyObject header is a target for optimisation. Performance regressions are likely to be noticed, and if a different header layout is faster, then it's entirely possible that it will be used for purely empirical reasons. Trying different options and picking the best performing one is not luck, even if you can't explain why it's the best performing.
You can expect the Python developers to look very closely at any benchmark that significantly benefits from adding random padding to the object header. Performance isn’t just trying a bunch of random things and picking whatever works the best, it’s critical to understand why so you know that the improvement is not a fluke. Especially since it is very easy to introduce bias and significantly perturb the results if you don’t understand what’s going on.
We're not talking about random changes. We're talking about paying attention to the measured performance of changes made for other reasons.
Just like in this article. The author measured, wondered, investigated, experimented, and finally, after a lot of hard work, made the C/Rust programs faster. You wouldn't call that luck, would you? If there had been a similar performance regression in CPython, then a benchmark could have picked up on it, and the CPython developers would then have done the same.
You can look at the history of PyObject yourself: https://github.com/python/cpython/commits/main/Include/objec.... None of these changes were done because of weird CPU errata that meant that making the header bigger was a performance win. That isn't to say that the developers wouldn't be interested in such effects, or be able to detect them, but the fact that the object header happens to be large enough to avoid the performance bug isn't because of careful testing but because that's what they ended up for other reasons, far before Zen 3 was ever released. If it so happened that Python was affected because the offset needed to avoid a penalty was 0x50 or something then I am sure they would take it up with AMD rather than being content to increase the size of their header for no reason.
What you don't see in the logs are the experiments and branches that weren't pursued further because they didn't perform well enough.
Also: If you're going to prove that changes informed by performance measurements are absent from the commit logs, then you'll need to look in the logs for all the relevant places, which means also looking at I/O and bytes and allocator code.
Given that the performance is only affected by the size of that object header, the file I linked is all you'd need to see changes in. Look, the Python project is not picking their object sizes because it performs well on a quirk of Zen 3. End of story. I did performance work professionally in the past and now recreationally and this specific instance is 100% luck. This is not because I don't think the runtime people aren't smart or anything but this would be an insane thing to do on purpose.
I think the confusion comes from people not having a good understanding of what an interpreted programming language does, and what actual portion of time is spent in high versus low level code. I've always assumed that most of my programs amount to a bit of glue thrown in between system calls.
Also, when we talk about "faster" and "slower," it's not clear the order of magnitude.
Maybe an analysis of actual code execution would shed more light than a simplistic explanation that the Python interpreter is written in C. I don't think the BASIC interpreter in my first computer was written in BASIC.
Agreed. The speed of a language is reverse proportional to number of CPU instructions emitted to do something meaningful, e.g. solve a problem. Not whether it can target system calls without overhead and move memory around freely. That's a given.
>I don't understand why Python gets shit for being a slow language when it's slow but no credit for being fast when it's fast just because "it's not really Python".
What's there to understand? When it's fast it's not really Python, it's C. C is fast. Python can call out to C. You don't have to care that the implementation is in another language, but it is.
I constantly get low key shade for choosing to build everything in Python. It’s really interesting to me. People can’t break out of thinking, “oh, you wrote a script for that?”. Actually, no, it’s software, not a script.
99% of my use cases are easily, maintainably solved with good, modern Python. The Python execution is almost never the bottleneck in my workflows. It’s disk or network I/O.
I’m not against building better languages and ecosystems, and compiled languages are clearly appropriate/required in many workflows, but the language parochialism gets old. I just want to build shit that works and get stuff done.
What happened to OP is a pure chance. CPython's C code doesn't even care about const-consistency. It's flush with dynamic memory allocations, bunch of helper / convenience calls... Even stuff like arithmetic does dynamic memory allocation...
Normally, you don't expect CPython to perform well, not if you have any experience working with it. Whenever you want to improve performance you want to sidestep all the functionality available there.
Also, while Python doesn't have a standard library, since it doesn't have a standard... the library that's distributed with it is mostly written in Python. Of course, some of it comes written in C, but there's also a sizable fraction of that C code that's essentially Python code translated mechanically into C (a good example of this is Python's binary search implementation which was originally written in Python, and later translated into C using Python's C API).
What one would expect is that functionality that is simple to map to operating system functionality has a relatively thin wrapper. I.e. reading files wouldn't require much in terms of binding code because, essentially, it goes straight into the system interface.
> The basics are seriously optimized for typical use cases, take a look at the source code for the dict type
Python is well micro-optimized, but the broader architecture of the language and especially the CPython implementation did not put much concern into performance, even for a dynamically typed scripting language. For example, in CPython values of built-in types are still allocated as regular objects and passed by reference; this is atrocious for performance and no amount of micro optimization will suffice to completely bridge the performance gap for tasks which stress this aspect of CPython. By contrast, primitive types in Lua (including PUC Lua, the reference, non-JIT implementation) and JavaScript are passed around internally as scalar values, and the languages were designed with this in mind.
Perl is similar to Python in this regard--the language constructs and type systems weren't designed for high primitive operation throughput. Rather, performance considerations were focused on higher level, functional tasks. For example, Perl string objects were designed to support fast concatenation and copy-on-write references, optimizations which pay huge dividends for the tasks for which Perl became popular. Perl can often seem ridiculously fast for naive string munging compared to even compiled languages, yet few people care to defend Perl as a performant language per se.
Raymond Hettinger's talk Modern Python Dictionaries: A confluence of a dozen great ideas is an awesome "history of how we got these optimizations" and a walk through why they are so effective - https://www.youtube.com/watch?v=npw4s1QTmPg
Yeah, I had a nice chat with Raymond Hettinger at a Pycon in Birmingham/UK back in the days (had no idea who he was at the time). He seemed like a dedicated and intelligent person, I'm sure we can thank him for some of that.
> Have you ever attempted to write a scripting language that performs better?
No, because "scripting language" is not a thing.
But, if we are talking about implementing languages, then I worked with many language implementations. The most comparable one that I know fairly well, inside-and-out would be the AVM, i.e. the ActionScript Virtual Machine. It's not well-written either unfortunately.
I've looked at implementations of Lua, Emacs Lisp and Erlang at different times and to various degree. I'm also somewhat familiar with SBCL and ECL, the implementation side. There are different things the authors looked for in these implementations. For example, SBCL emphasizes performance, where ECL emphasizes simplicity and interop with C.
If I had to grade language implementations I've seen, Erlang would absolutely take the cake. It's a very thoughtful and disciplined program where authors went to a great length to design and implement it. CPython is on the lower end of such programs. It's anarchic, very unevenly implemented, you run into comments testifying to the author not knowing what they are doing, what their predecessor did, nor what to do next. Sometimes the code is written from that perspective as well, as in if the author somehow manages to drive themselves in the corner they don't know what the reference count is anymore, they'll just hammer it until they hope all references are dead (well, maybe).
It's the code style that, unfortunately, I associate with proprietary projects where deadlines and cost dictate the quality, where concurrency problems are solved with sleeps, and if that doesn't work, then the sleep delay is doubled. It's not because I specifically hate code being proprietary, but because I meet that kind of code in my day job more than I meet it in hobby open-source projects.
> take a look at the source code for the dict type.
I wrote a Protobuf parser in C with the intention of exposing its bindings to Python. Dictionaries were a natural choice for the hash-map Protobuf elements. I benchmarked my implementation against C++ (Google's) implementation only to discover that std::map wins against Python's dictionary by a landslide.
Maybe Python's dict isn't as bad as most of the rest of the interpreter, but being the best of the worst still doesn't make it good.
Except it is, because everyone knows sort of what it means, an interpreted language that prioritizes convenience over performance; Perl/Python/Ruby/Lua/PHP/etc.
SBCL is definitely a different beast.
I would expect Emacs Lisp & Lua to be more similar.
Erlang had plenty more funding and stricter requirements.
C++'s std::map has most likely gotten even more attention than Python's dict, but I'm not sure from your comment if you're including Python's VM dispatch in that comparison.
(std::map is famously rubbish, to the extent that a common code review fix is to replace it with std::unordered_map. Map is a tree, unordered map is a linked-list-collision hashtable. Both are something of a performance embarrassment for C++. So std::map outperforming a given hashtable is a strongly negative judgement)
If I may hazard a guess (I don't know why the original code used it), Python dictionaries are also ordered (and there's no option in Python's library to have them not ordered). Maybe they wanted to match Python's behavior.
Python dicts are ordered in a slightly weird way though, and only since 3.6 (before that it leaked the hashtable implementation scheme). Std::map orders things by some boolean comparator (and duly goes wrong if you give it a partial order comparison), binary tree style. Python currently orders by insertion order, very like storing a list of the keys as well as a hash table, so it can iterate over it in insertion order. That's definitely slower than not maintaining that order but it seems popular with python developers.
Do you mean "insertion ordered"? That means the order of iteration is guaranteed to match insertion order. C++'s std::map is ordered by key (less than comparison) to create a binary search tree. So iteration order will always be ordered by key value. C++'s std::unordered_map has no ordering guarantees (that I know). I don't think the standard C++ template library has the equivalent of a modern Python dict, nor Java LinkedHashMap. Does anyone know if that is incorrect?
Unordered map sure hasn't been. There are algorithmic performance guarantees in the standard that force the linked list of buckets implementation despite that being slower than alternatives. Maybe the libc++ map<> is a very perfect rbtree, but I doubt that's the state of the art in ordered containers either.
There is no such thing as interpreted language. A language implementation can be called an interpreter to emphasize the reliance on rich existing library, but there's no real line here that can divide languages into two non-ambiguous categories. So... is C an "interpreted language"? -- well, under certain light it is, since it calls into libc for a lot of functionality, therefor libc can be thought of as its interpreter. Similarly, machine code is often said to be interpreted by the CPU, when it translates it to microcode and so on.
> prioritizes convenience over performance
This has nothing to do with scripting. When the word "scripting" is used, it's about the ability to automate another program, and record this automation as a "script". Again, this is not an absolute metric that can divide all languages or their implementations into scripting and not-scripting. When the word "scripting" is used properly it is used to emphasize the fact that a particular program is amenable to automation by means of writing other programs, possibly in another language.
Here are some fun examples to consider. For example, MSBuild, a program written in C# AFAIK, can be scripted in C# to compile C# programs! qBittorrent, a program written in Python can be scripted using any language that has Selenium bindings because qBittorrent uses Qt for the GUI stuff and Qt can be automated using Selenium. Adobe Photoshop (used to be, not sure about now) can be scripted in JavaScript.
To give you some examples which make your claim ridiculously wrong: Forth used to be used in Solaris bootloader to automate kernel loading progress, i.e. it was used as a scripting language for that purpose, however most mature Forth implementations are aiming for the same performance bracket as C. You'd be also hard-pressed to find a lot of people who think that Forth is a very convenient language... (I do believe it's fine, but there may be another five or so people who believe it too).
---
Basically, your ideas about programming language taxonomies are all wrong and broken... sorry. Not only you misapplied the labels, you don't even have any good labels to begin with.
Anyways,
> What are you trying to prove here?
Where's here? Do you mean the original comment or the one that mentions std::map?
If the former: I'm trying to prove that CPython is a dumpster fire of a program. That is based on many years of working with it and quite extensive knowledge of its internals of which I already provided examples of.
If it is the later: parent claimed something about how optimized Python's dictionary is, I showed that it has a very long way to go to be in the category of good performers. I.e. optimizing something, no matter how much, doesn't mean that it works well.
I don't know what do you mean by Python's VM dispatch in this context. I already explained that I used Python C API for dictionaries, namely this: https://docs.python.org/3/c-api/dict.html . It's very easy to find equivalent functionality in std::map.
For starters, since everything in Python is a pass-by-ref object, dicts store pointers to values, which then have to be allocated on the heap and refcounted, whereas std::map can store values directly. But this is the consequence of a very-high-level object model used by CPython, not its dict implementation that has to adapt to that.
You are very confused between how something works right now and how it can work in principle. In this you very much resemble CPython developers: they never attempt optimizations that go beyond what Python C API can offer. This is very limiting (and, this is why all sorts of Python JIT compilers can in many circumstances beat CPython by a lot).
The evidence to how absurd your claim is is right in front of you: Google's implementation of Protobuf uses std::map for dictionaries, and these dictionaries are exposed to Python. But, following your argument this... shouldn't be possible?
To better understand the difference: Python dictionary stores references to Python objects, but it doesn't have to. It could, for example, take Python strings and use C character arrays for storage, and then upon querying the dictionary convert them back to Python str objects. Similarly with integers for example etc.
Why is this not done -- I don't know. Knowing how many other things are done in Python, I'd suspect that this isn't done because nobody bothered to do it. It also feels too hard and to unrewarding to patch a single class of objects, even as popular as dictionaries. If you go for this kind of optimizations, you want it to be systematically and uniformly applied to all the code... and that's, I guess, how Cython came to be, for example.
Cython is pretty much Python without bytecode interpreter, translated to C instead, but retaining the object model. That's why it's so slow.
And the reason why the object model is the way it is, is because it's an entrenched part of the Python ABI. Sure, if you break that, you can do things a lot faster - this isn't news, people have been doing this with projects like Jython and IronPython that can work a lot faster. But the existing ecosystem of packages is so centered on CPython that this approach has proven to be self-defeating - you end up with a Python implementation that very few people actually use.
So, no, it's not because people are "very confused" or "nobody bothered to do it". It's because compatibility matters.
Well, again, you are confused... and, most likely the CPython developers didn't bother.
No. You don't need the Python object model when implementing Python dictionary. You have evidence right in front of you: std::map bindings are successfully used in its place.
Why even keep arguing about this?
In fact, you can implement your own dictionary, and if you expose all the same mapping protocol, it will work the same as the built-in one. Do you have to use Python objects for this? -- absolutely no. You can convert at the interface boundary. Experience shows that this works noticeably better than using Python objects all the way. Why did the original CPython developers not do it? -- I don't know, can only guess. I already wrote what my guess is. And, in all sincerity, CPython has a lot more and a lot worse problems. Compared to the rest of the codebase, the dictionary object is fine. So, if anyone would seriously consider improving CPython's performance they wouldn't touch dictionaries, at least not at first.
You don't need a Python object model when implementing a highly specialized dictionary that can only store very specific data types. But Python dict is a generic collection type that is designed to store any Python object (or rather, reference to such, since Python has reference semantics for anything).
And this part:
> if anyone would seriously consider improving CPython's performance they wouldn't touch dictionaries, at least not at first.
is just straight up nonsense, given how many times over Python's history dicts have been substantially rewritten. As it happens, I work on Python dev tooling, and the CPython team changing internal data structures for perf reasons has been a recurring headache for me, so I know full well what I'm talking about here.
> Have you ever attempted to write a scripting language that performs better?
Way to miss the mark. The point is precisely that Python is slow and one of the causes is that it is a scripting language. Stomping your foot and essentially: "You couldn't do any better" helps no one and is counterproductive.
The premise is that any time you say "Python [...] faster than Rust [...]" you get page views even if it's not true. People have noticed after the last few dozen times something like this was posted.
This is the answer. The thread is chasing various smart-people opinions about languages, interpreters, system calls. We got tricked into click bait title and are using the opportunity to rehash our favorite topics and biases.
I'm not surprised the conclusion had something to do with the way that native code works. Admittedly I was surprised at the specific answer - still a very interesting article despite the confusing start.
Edit: The conclusion also took me a couple of attempts to parse. There's a heading "C is slower than Python with specified offset". To me, as a native English speaker, this reads as "C is slower (than Python) with specified offset" i.e. it sounds like they took the C code, specified the same offset as Python, and then it's still slower than Python. But it's the opposite: once the offset from Python was also specified in the C code, the C code was then faster. Still very interesting once I got what they were saying though.