Software is slow because of bad design, not slow languages
41 points by nrposner
41 points by nrposner
Consider a simple CRUD website with a database. … In other words, most real programs are “I/O-bound”
Sigh. Another server-side developer who thinks all “real programs” are servers.
Outside of servers there are definitely some IO-bound programs (or parts of programs) but a lot that aren’t. A web browser's layout engine, a 3d renderer, a word processor, a neural network…
It's also incorrect for server-side. The myth of the "I/O-bound" workload is strong in backend development unfortunately. Fetching data from DBs takes ~5ms but somehow average responses take ~500ms. There is a lot of CPU bound mini workloads and overhead people take for granted.
Memory usage is an additional consideration. We expect a Go program to use an order of magnitude less memory than an analogous Python program. For demanding applications we may expect an optimized Rust program to use an order of magnitude less memory than the Go program. In server-side contexts this reduces the demand for horizontal scaling.
Yes, great point with horizontal scaling. Which brings me to another thing I'd like to highlight: A Ruby/Python program will force you into design choices that a Rust/Go program will not because it can't scale vertically.
At some point you'll be forced to throw more machines at the problem which then means you'll need: application load balancers, shared networked caching, shared networked file systems, networked inter-process communication in the form of API/gRPC/Kafka, and a whole lot more.
All of that adds latency. In a faster language a lot of that can be done in a single binary. So to escape a CPU/Memory bound problem we've incurred a lot of I/O-bound problems. Which is another reason why people think all server-side problems are I/O.
Yes!
And even in scenarios where memory usage would be similar (say, working with pointers to large in-memory blob, say in an image editor), an advantage of Rust and the other non-GC languages here are the more consistent- and predictable- memory profile, because then you can allocate resources accordingly. It's very advantageous to be able to confidently know something will use <2GiB memory.
And when you do find yourself scaling, or running a "serverless function", etc... It's hugely advantageous to have mostly-static binaries and to have no runtime to spin up. (So, Rust beats Go beats Python here).
I love Python, but it's certainly the loser of performance competitions.
It used to be a lot more correct for server-side than it is now, in the age of spinning disks. Circa 2006, Django requests were definitely usually dominated by database queries, and this was widely pointed out to people who complained about the speed of template formatting. That's probably very rarely the case now.
Yeah, it's interesting to me how we still largely design systems today to fit the design constraints of the late 90s.
Even relative to CPU speed, I/O speed has increased a lot, threads are orders of magnitude more performant, and memory access has gotten much more expensive.
Even things like big-O analysis of algorithms that don't do any I/O aren't as simple as they were in the 90s. Nowadays an O(n^2) algorithm with a very CPU-cache-friendly memory access pattern can beat an O(n log n) one for surprisingly high values of n. In the 90s we didn't have a 100x performance difference between memory reads depending on whether nearby memory has been recently accessed. (Though I guess if you squint, accessing swapped-out pages might sort of qualify.)
And "O(1)" algorithms are actually roughly O(sqrt(n)), and "O(n)" algorithms are actually O(n sqrt(n)). As your data set grows, they spill out into larger, slower kinds of memory: L1 -> L2 -> L3 -> RAM -> local SSD -> local HDD -> NAS, giving a roughly sqrt(n) size/speed trade-off.
(I may be getting the actual function long, maybe it was log(n) and not sqrt(n), I got this from a talk I watched a while ago; but the idea is correct even if the specific function isn't.)
Yeah, there are both theoretical and empirical reasons for memory access being O(sqrt(n)) (previously, previously).
As well as the raw cost of the memory hierarchy, there’s the cost of virtual address translation which is about O(log(n)) (as you might expect based on it being implemented using a tree walk).
(PS. good grief it took me a long time to dredge up the magic keywords to find that “myth of RAM” blog series, and I have read it at least twice…)
And "O(1)" algorithms are actually roughly O(sqrt(n))
One of the beefs I had with big-O when I was introduced to it in my CS classes in the 90s was that, as commonly practiced, it papers over some significant costs. Like a sorting algorithm that's presented as O(n log n) only has that complexity if comparing two elements is a constant-time operation, which it isn't if you're, say, comparing arbitrary-length strings that can have common prefixes.
My college roommate and I invented a variant on radix sort for linked lists of variable-length values (by accident; neither of us had learned about any existing radix sort algorithms yet) and we were frustrated that using the analysis techniques we'd learned in class, it was an O(n) sorting algorithm, but the actual execution time clearly depended on the lengths of the values, not just the number of values.
I realize some of that is just "we give undergrads the simplified version" but I'd wager most people who learned big-O at all didn't learn about the more sophisticated, comprehensive approach to it.
One of the beefs I had with big-O when I was introduced to it in my CS classes in the 90s was that, as commonly practiced, it papers over some significant costs.
(From below.)
When I first encountered “big-O,” it was presented in the context of Bachmann-Landau notation as a mathematical tool for characterising the behaviour of mathematical functions that had practical applications as a tool to understanding the performance of a computer programme.
From this perspective, it seems obvious that there is a considerable amount of judgement necessary to employ the tool to support decision-making. The limitations of this tool (e.g., the practical consequence of ignoring constant factors) was discussed quite thoroughly, but it seems fairly obvious that there is a high degree of abstraction inherent to any discussion of performance, and no single analytical tool or metric can, in isolation, justify a decision in the human world. (Human existence, and the universe we inhabit, is simply too infinitely profound.)
As a result, while I think details like cache memory and branch prediction are very worthwhile to consider, I'm not sure they detract from the value of the “big-O” as a tool, since we have never been able to justify statements like “this is better than that, because O(n) is better than O(n²).”
aren't as simple as they were in the 90s
I am reminded of a scene from David Hare's Turks & Caicos:
MAITLIS [Kobna Holdbrook-Smith]: Because that's the way of things now. Mr Eliott [Bill Nighy], I used to be able to open the bonnet, take out a wrench, and fix my car. Now I'd need a degree in electronics. Even easy things are difficult now.
Not only is the execution environment dramatically more complex and, thus, harder to analyse than it was in the 1990s, but more than a few codes written in the 1990s are still in use today.
But I think this further reïterates both the usefulness of (and, of course, the limitations of) “big-O” as an analytical tool.
If I were to write codes today that I expected to still be in use in twenty years, it would be impossible for me to account for future changes in execution environment. (e.g., if I write a lookup table to avoid a computation, because my CPU is slow, but then ten years on, my code is slower than if I had not done that, because my CPU is now faster than memory… well, what happens when this flips yet again sometime in the following decade?)
As a mathematical formulation, the Bachmann-Landau functions are definitely less effective at justifying immediate decisions, but they may be more durably correct over time. Of course, we want these analyses to not merely be correct but also be useful, and we may find that to be the case if we can make confident predictions about the monotonic growth of the data.
So, while I cannot, in any way, disagree that “big-O” is a very rough tool for qualifying the performance of a software programme, (I don't think it's any less rigorous than what people actually do and…) I think there are more obvious, more significant, and more consequential failings in how we support decision-making around performance.
And of course the database also uses the CPU...so just because something is database-bound doesn't mean it is also I/O-bound.
Each call is ~5ms, but a lot of written server software ends up doing 20-50 calls to the DB between bad ORMs and layers of helpers (validate the user, load the user, load related claim, load...).
At a past job of mine where we were especially latency-sensitive, we built this complicated dependency-graph-aware promises-and-futures system for deferring database queries until nothing could proceed any further, then attempting to combine as many of the deferred queries as possible to minimize database round-trips. It was very successful at keeping the "20-50 sequential DB queries" problem at bay, but it was a real beast to write and maintain, and it made the application code significantly more convoluted.
It's not an easy problem to solve for nontrivial code bases.
Yes. And even on servers it really hasn't been true for a while. SSDs are fast, and even the I/O paths of many programs are actually CPU-bound.
Case in point: mkfile(8). A program that does nothing except write zeros to disk. CPU-bound on macOS. And not by a little bit either.
mkfile(8) is severely syscall limited on OS X
Case in point: reading JSON. I noticed that when doing some measuring on our Wunderlist iOS and macOS clients. The I/O was not I/O bound. It was CPU bound. I was surprised.
Side note: talking about or acting on performance without measurements is usually pointless and leads to comically wrong results like the ones in this blog post.
Swift, a language some consider "fast" managed JSON parsing at a blazing 10 MB/s for its built-in mechanism, which people who didn't measure thought was fast. With a less braindead design and Objective-C instead of Swift, I got to around 200MB/s, which was close to the pure rate of object allocation. Even the SSD at the time did 2 GB/s, so 10x what I managed and 200x what Swift did out of the box. So 99.5% CPU, 0.5% I/O for Swift. 90% CPU and 10% I/O for me.
Somewhat Less Lethargic JSON Support for iOS/macOS, Part 1: The Status Quo
This discrepancy was also what "inspired" David Lemire to create the fantastic simdjson library, which does actually manage to make things I/O-bound, but at that time at the cost of not offering a direct object-generation hook. So you had to first parse the entire file to an intermediate representation and then walk that to get your objects. I haven't checked whether that is still the case.
Agreed, and I can back up your numbers on JSON. For my streaming implementation of simdjson, per-source throughput tops out in the 4-5GB/s range, and that's pulling all the stops, using many threads inefficiently on a single source. Using a single thread efficiently, you're down to the 2-3GB/s range.
If your data fits entirely in RAM and is already loaded, you can get better throughput while using threads efficiently (as the original simdjson does), and if you know that your data is valid NDJSON on top of that, then you can meaningfully parallelize and crack 10+GB/s, but you need to throw a bunch of threads at the problem under very particular conditions before the problem is meaningfully I/O-bound on a modern SSD.
Last I was aware simdjson still did tape building and tape walking in separate steps, though the On-Demand variant API lets you build the tape progressively, without requiring the source file be all in memory up front.
On most hardware, this problem (at least the non-NDJSON streaming version) is actually bottlenecked on the SIMD: the lanes aren't wide enough. I got an experimental version of this running on a discrete GPU, simulating super-wide lanes (give or take some hacks to the classification algorithm), which is I/O-bound... but on the PCIE bridge, not on memory->RAM, the throughput on a T4 wasn't high enough (6GB/s each way IIRC). I haven't gotten it working on an integrated GPU yet, but once I do, I expect it will still be CPU-bound, just on the bitwalking+parsing step rather than byte classification.
Yes, and now that ML (not even LLMs) is becoming a larger part of the industry and constitutes a more popular set of programs, I find this especially funny.
Even before LLMs, I think for the very simple case of "I have some data and I want to create a presentation layer that allows CRUD operations on it", programs were already fairly trivial. While those kinds of programs may make up a majority of user-hours, they were trivial to write. Most developer-hours were (according to my anecdotal estimation at least) spent on the much harder parts of programs and large systems like the things you mention.
They said “most,” not all.
I think client-server application are ubiquitous and this is fair to say.
I'm going to ignore that this argument mostly focuses on CRUD services, I think for CLIs where latency is directly user-visible just by typing --help, a lot of slow languages have already lost (except nodejs for whatever reason)
Slow languages already delegate work to fast languages
Passing data between languages is not easy, and a lot of problems are not amenable to it in the same way that "convert one file format to another" is (because at the FFI layer that is just, string in, string out)
For example: You can swap out a pure-Python JSON parser for rapidjson or ujson, but a lot of time will still be spent creating Python objects, not parsing. You could use msgspec but there is still substantial overhead compared to RIIR.
Caching a slow operation (typically an external API call)
Caching it where? In Python I would reach for memcached or redis because in-process caches don't survive very long and are not shared across concurrency units. I don't have that problem in Rust webservices and you can get very far with in-memory hashmaps there.
Giving up, he simply wrote the program in Rust in the naive way, expecting a 20% slowdown or worse. Instead, it was 30% faster [...] So even in a largely CPU-bound workload, in the hands of someone very skilled at writing fast C, performance is dominated by design, not by language speed.
The "naive way" worked in Rust because of design choices made in stdlib, but wouldn't work out in slower languages.
As shown in the example above, some designs are more feasible in one language vs another. Language choice is a choice of ecosystem, and different ecosystems have different paths of least resistance. Personally, I've had to bend over backwards to achieve acceptable performance in Python more often than in Rust.
Yep, I’ve spent a good chunk of my career fixing problems in projects that picked Python because “if they need performance they’ll just rewrite the slow parts in C/Rust/etc” without realizing that they are making the slow parts nominally fast but adding on a ton of new time spent marshaling data between languages (I’ve seen performance regress for this reason). The only way “rewrite the slow parts” works is if you commit to using the guest languages’s native data structures everywhere in your Python code, which is typically such a massive rewrite that you’re better off just committing to the guest language (rather than maintaining bespoke build tooling for a multilingual application).
The only way “rewrite the slow parts” works is if you commit to using the guest languages’s native data structures everywhere in your Python code, which is typically such a massive rewrite that you’re better off just committing to the guest language
No, I wouldn't call that the only way. Sometimes you can get away with designing your API in such a way that the serialization cost is very low. Sometimes that goes against good taste, and leaves you with very weird separation of concerns, but it works.
A simple example is rapidjson.Validator: https://python-rapidjson.readthedocs.io/en/latest/validator.html
Its API is specifically designed to reduce FFI overhead, by just passing strings. I'm not saying this is bad design, but it is not something I would design that way if the Validator was written in the host language.
Personally, I've had to bend over backwards to achieve acceptable performance in Python more often than in Rust.
Agreed; and, to be precise, I've routinely had to engage in bad software engineering practices to speed up Python performance. Things like removing abstractions, copying and pasting code, not using newtype wrappers, etc. At this point in my career I don't want to engage with this tradeoff unless absolutely necessary.
Python typing made a lot of this better if you as an API owner and your users can fully commit to it. You can now send raw data structures around and hide them behind whatever type annotation you like, so that the type checker throws if somebody other than you touches your dict or tuple.
except nodejs for whatever reason
I'm getting up there in years. But I'm really hoping this dies off in popularity before I go!
I'll raise one point of disagreement with this post: I don't think 'writing in a slow higher-level language can make it much easier to optimize the code, because you can see at a glance what the program is doing.'
That would certainly be the case with a program written in assembly, and likely also in C, but I'm now sure if that can be attributed to them being 'low level'.
But my issues with optimizing programs in higher level languages, generally Python, is that I wind up wrestling with the language or packages to figure out what's actually happening under the high-level abstractions. I've also found that the designs encouraged by higher-level languages, especially with lots of inherited classes and indirection and spooky action at a distance, tends to make the overall structure of the program more confusing (I'm looking at you, Julia!) and things like rereading the same file thousands of times in a loop instead of once can sneak in more easily.
I would attribute the difficulties more to dynamic typing/poor type systems and runtime dispatch that can't be checked at compile time.
I also find that, when understanding and fixing the bad design of an existing program in a higher-level language, rewriting (parts of) the program to a language that offers more low-level visibility is a great help in untangling the design and figuring out the data flow. I find that Rust is generally a good fit for this, since you can move very quickly between low level control and high level abstraction, and the ownership system highlights tangled dependencies.
I'll raise one point of disagreement with this post: I don't think 'writing in a slow higher-level language can make it much easier to optimize the code, because you can see at a glance what the program is doing.'
In my experience, it has never been the case when I can correctly call where the time is spent in a program (regardless of language). My own track record when guessing where the time is spent is about 50%. The other 50% of the time I'm completely surprised at the result.
The other aspect of slow code I've noticed is the sheer number of abstraction layers one has to go through to get to the few lines of code that do the actual work, again, regardless of language.
I think this applies more to something like Rust, which sometimes can be faster than C because of the extra info the compiler has.
The author has a point that big design or algorithmic changes can matter a lot. But if you're rewriting Ruby, Lua, Perl, PHP, or most Python into basically anything else, it's going to be way fucking faster. The author doesn't just leave this out, he actively eschews it. That's wrong, and only explainable by reason of willful ignorance.
See any of the language shootouts: https://benchmarksgame-team.pages.debian.net/benchmarksgame/
Since you're rewriting it anyway, redesign at the same time. By the third implementation I usually feel like the design is sorted. That holds pre-AI and now.
Totally agree. I have used C# on nearly every project I've done for the past 10 years (when it got good Linux support). While there has certainly been dumb moves by me that have caused slowness per request, in general I never think much about scaling, even at very high rps. Some of this is certainly experience in design but a lot (most?) of it comes from a framework that is fast (in general C# is as fast or faster than golang, if you wondered what I was talking about!).
I then get called in to help speed up $DYNAMIC_LANGUAGE and it's often a total horror show, with dozens of app servers maxxing CPU and memory, constant fire fighting of load based outages, and complicated layered caching structures which cause weird edge cases.
In general the core issue imo is that ruby, python, node et al do not handle threads efficiently. Back in the day, this wasn't much of an issue, you may have 1, 2 or maybe 4 CPUs on a server. Now you have 16 cores on a cheap phone. So you end up having to run lots of separate processes to take advantage of the hardware.
You then put on top of this in general they have far higher RAM usage per process because they are not compiled, and before you know it you are using 10x the RAM. And because you are using 10x the RAM you get constant GC events which starts burning all the CPU .
Then you just rewrite it in a statically typed languages with a good threading model and suddenly the same app is barely using 10% of your only app servers CPU. I've seen it so many times.
There are many good reasons to rewrite software in a different language: portability, efficiency, maintainability, and so on. But in terms of raw speed, you’re better off focusing on the program’s design.
I think the reason we’re seeing so many “we asked an LLM to rewrite our service in X and it got faster” is precisely because people don’t want to think about their programs design. It’s rarely a fair comparison
It is almost like the actual design aspect of programming was the much harder part and the 'writing out the syntax' part wasn't (as hard).
Python has entered the chat
Python has entered the chat
This comment is meant to be flippant and meant to be a joke, but I think it's worth hearing a bit from the cohort of Python users who are very performance sensitive—those of us who do data analysis or numerical computing.
Codes that rely on NumPy are often dismissively viewed as just “bolting” C onto Python, and the performance of the resulting system is, in my view, incorrectly attributed wholly to the capabilities of C. This really underestimates the amount of (evolved) “design” in these systems.
This is why I have maintained that the “restricted computation domain” formulation is useful when thinking about these systems. The idea that the “heavy lifting” or “number crunching” gets delegated to some C or Fortran codes (e.g., the aforementioned NumPy,) while the “programme structuring” or “business logic” happens within Python is an interesting but not particularly deep observation.
I don't think there's is much debate over whether Python semantics provide the ability to express information currently necessary to achieve performance on contemporary hardware—the deliberate choices (e.g., automatic integer promotion) and the missing pieces (e.g., control over memory layout) are quite well-documented. It remains to be seen whether these can be inferred or otherwise synthesised through code improvements (e.g., reducing the number of C-level stack frames required to perform a Python-level function call) and JIT (which, as I understand from talking to the core developers, has been successful in some benchmarks but has introduced some other undesirably complexity) and other approaches but, historically, the CPython interpreter just hasn't been particularly fast at doing some fundamental operations.
Where the “restricted computation domain” formulation goes beyond simpler descriptions of Python as a glue language or observations that limitations in Python force heterogeneous computation (i.e., some in Python, some lower,) is that these entities require a significant amount of design to actually work. While a lot of the popular tools did not undergo a particularly extensive or deliberate design process, there were evolutionary factors at play that forced them to be mostly effective at encoding computations. However, it is actually not uncommon to “fall out” of the design of these tools.
For example, I spoke some years ago at PyData Amsterdam about “group-weighted summary statistics” (e.g., group-weighted standard deviation or z-transform with high cardinality of groups) using the popular pandas DataFrame library. From the most naïve approach using just pandas to a very contorted approach using just pandas to an approach relying on cython, I recall getting a 1× → 10× → 100× improvements. Similarly, when talking about developing simulators with Python and pandas, we must contend with computations that cannot readily be encoded into the tools themselves, and given the (polarised or dichotomous in nature) heterogeneity of these tools and the degree to which they cannot readily be decomposed, we end up having little choice but to accept the subsequent contortions.
All of this is to say… I genuinely simultaneously believe that:
In fact, regarding the last point, a common refrain on a recent simulator project I worked on was that, if we had been involved in the original writing of that system, we could have written it in Python (and, of course, with C or Cython or C++ or Rust or Zig,) and it would have been easier to extend, debug, less code overall, and at least an order of magnitude faster if we had taken a naïve approach (and maybe even two or three orders of magnitude faster with a little sophistication…!!)
That means that I both agree and disagree with the thesis of this post. Python, as a language, doesn't lend itself to being fast. Many Python programmes are not fast. Some or even many Python programmes are not fast, and gain nothing from using Python, where a line-for-line rewriting of them in another language would be faster. But the use of Python does not inevitably lead to systems that are slow, and, in fact, the necessary design to produce fast systems in Python may not (in terms of smarts and efforts and luck) be significantly more than what is necessary to achieve the same in, say, C++.
Architecture and design are crucial. Processes, data flows, patterns… this matters most. Much more than particular technology.
Regarding the languages: besides „fast“ languages and „slow“ languages (actually a glue that connects libraries written in C usually), there is a third category of languages like Java. It is common to write everything in pure Java including the libraries, networking frameworks etc. Only few parts that need to interact with hardware or are platform-dependent are common to be native binary. Even performance-heavy software is written in Java (or other JVM language). Java gives you a comfortable high-level language with generics, annotations, reflection, reasonable amount of dynamism, a great tool for building frameworks and abstractions. It is not a silver-bullet, but a very good fit for business software and server-side services.
I can corroborate the java part. A proprietary enterprise chonker I'm very familiar with manages fine just vertically scaling the WebSphere nodes, it's the terribly designed relational DB (meaning grave misuse of OLTP for quite heavy OLAP, and a schema that's not great) that's causing incredible problems for everyone.
Misses the main thing: slow languages don't exist. Languages aren't slow. Slow implementations exist, and can be replaced over time by faster implementations.
I don't think this is correct.
Changes to implementation can absolutely have big effects on performance within a single language. CPython gets faster with most new version as the interpreter and the standard library improve, and you can step out CPython into Jython, or IronPython, or JITted PyPy, and see very different performance characteristics while remaining recognizably the same language. We might even conceive of an OCaml-like dual interpreter/compiler that can do both.
But there are differences in language semantics which mere implementation details can't bridge.
A language whose semantics include loose or dynamic typing will place restrictions on the implementation that will reduce performance. A language whose semantics relies on dynamic dispatch without the ability to monomorphize will see unavoidable overhead. A language which abstracts away memory allocation takes a lot of optimization tools away from the program author, etc.
Changes to core language semantics like these would be extremely disruptive even in a whole-version migration breaking backwards compatibility. Adding required static typing to a dynamic language or manual memory management to a previously GC'ed language would be such a large difference that you almost certainly can't call it the same language before and after.
You can of course write extensions in faster languages, but at that point you're deliberately stepping outside your language's established semantics because you've acknowledged that they're too restrictive or insufficiently powerful to achieve your goals.
So I don't agree that slow languages don't exist. By virtue of their core semantics and the abstractions they aim to provide to the program author, all programming languages limit the performance they can deliver even with an ideal implementation.
A language whose semantics relies on dynamic dispatch without the ability to monomorphize will see unavoidable overhead.
I’m not sure there are languages that really can’t be monomorphized. In principle someone could implement such a dynamic language with a Self-style JIT that dynamically monomorphizes, so the code can be optimized to perform as well as static dispatch. But I suppose the JIT itself is unavoidable overhead :-)
Also, a dynamic language’s design can make it easier to monomorphize, by making it clear when the metalevel is being mutated (things like class definitions) and when those mutations take effect. But that kind of design is relatively rare: CLOS is designed that way, and I think Java too (tho Java relies more on dynamic reoptimization than CLOS).
A language which abstracts away memory allocation takes a lot of optimization tools away from the program author, etc.
Yeah, but I’d say memory layout is more important than allocation: if a program can control when subobjects are embedded in other objects and in arrays, and isn’t required to refer to them indirectly, then it’s easier to get better memory performance. This is one of the big advantages of Golang relative to Java. And project Valhalla, adding value types to Java, has taken many years of careful work to avoid being disruptive.
Data structure layout at a high level (how objects relate to each other, as opposed to the internal layouts of objects) is in almost all languages controlled by the programmer. Language semantics usually don’t allow data layouts to be changed by optimizers because it’s prevented by features like object identity and mutability and aliasing. So I think data layout is the primary thing that determines whether a language is “fast” in the sense of being able to get the most out of the hardware (assuming it isn’t held back by a slow implementation).
I don't believe e.g. Haskell typeclass instances can always be monomorphised:
pathological :: (Show t) => (Integer, t) -> String
pathological = \case
(0,x) -> show x
(n,x) -> pathological (n-1,(x,x))
Anybody who writes code like this is going to pay a cost in runtime performance, one way or another.
I agree with this take. I understand the point; that a language isn't its implementation, but language semantics absolutely do influence the implementations that can be built and also the implementations that are built.
The way I see it is that in theory, languages aren't fast or slow. If I have two programs in two different languages, but the semantics of these programs are equivalent, Sufficiently Smart Compilers might produce the same machine code. If the semantics aren't equivalent, comparing just the performance is misleading.
Of course, in practice, we don't have Sufficiently Smart Compilers. That's why we have tradeoffs between performance and other desirable quality attributes (e.g. correctness -- should the default integer type use bignum arithmetic?).
"In theory, there is no difference between theory and practice, while in practice, there is."
This is true in the same way that driving a car isn't necessarily faster than flying a plane because you can put a jet engine on the car and compare it to a propeller plane.