A practical comparison of build and test speed between C++ and Rust
quick-lint-js.com
quick-lint-js.com
I would like to mention my subjective concerns for Rust. I hope the Rust community can think me as a canary who is from the C++ world. I am desperately looking for an alternative to C++, and I am a mid-weight C++ programmer. And here is what Rust could face down the line (remember, in early days C++ too was a grockable language and hence its popularity):
0. Rust's C++ problem: Learning curve. From the get go if there is murmur that Rust has a learning curve then think what will happen when Rust reaches the maturity of C++.
1. Rust's NPM problem: supply-chain. If I decide to use npm, and use the npm toolchain to download a project then to my horror it downloads god knows what. Lots of dependency hierarchies, some of them have not even reached 1.0. This raises the concern for supply-chain attacks. Rust with the aim of "boosting developer productivity" just decided to follow the path of all those who are susceptible to supply-chain attacks.
2. Rust's Haskell problem. Haskell is a very beautiful language. Even then it has not reached the level of adoption of other popular languages. It has nothing to do with the language, but some in the community who have a tendency to project a coolness level by saying something like "A monad is just a monoid in the category of endofunctors". What that ultimately does is alienate lot of software developers. Early Rust evangelist tried very hard to project Rust as having a very welcoming community, but if acronyms and terms from category theory and lambda calculus, that are alien to most developers, are thrown around without taking into consideration the larger development community then I fear that Rust will go the Haskell or OCaml way i.e it will become a niche language with small community.
Just 2cents from an average everyday software developer.
Not every language evolution is a cautionary tale. Java, C#, PHP, JS have grown a lot and are fine.
Rust’s learning curve is not from accidental overgrowth of complexity (e.g. it has two+ string types not because of legacy, but because it needs different kinds of ownership). Rust has editions that make its evolution less constrained by backward compatibility, “how are we going to teach this?” is a mandatory question for every language change, and Rust has exceptionally helpful compiler errors.
Rust has some tooling for managing supply chain risks - vetting dependencies, vulnerability scanning, vendoring, etc. C++ doesn’t have any special security model or other solution that Rust lacks to make dependencies safe, only pain and annoyance that makes people avoid using dependencies. So it’s unclear what Rust could do better here.
Rust projects tend to split themselves into multiple small crates, so “lots of dependencies” in Cargo isn’t as heavy and reckless as same number of C++-sized deps.
Rust has already broken out of being a niche language.
Naming things is hard. Rust has some less common features and needs to refer to them by some name. Sometimes it borrows terms from other languages (like enum instead of a sum type), but sometimes there isn’t a common word for everything. But jargon isn’t the same as elitism. I do worry Rust will grow and have Eternal September eventually, but so far it’s welcoming for beginners, even if it needs to explain awful acronyms.
I find it very weird that anyone would think that any programming language is even remotely fine. Everything is a mess, there are terrible compromises to be made everywhere between expressivity, readability, build-time performance, run-time performance, safety, language community, etc etc. A language will be "fine" the day there's no compromise to be made anymore and it outclasses everything else before it on every metric relevant to shipping good software fast to end user while enjoying the process.
Fine is a synonym for "satisfactory" or "acceptable."
"Fine" in programming specifically, usually translates to "I can use it to solve my problem without the flaws in it inhibiting me from doing that" or "I can forget that I'm using the tool and just focus on solving the problem."
Many things in the world are "fine" by this definition. That doesn't necessarily mean they're all good, let alone perfect — but they're fine.
Copying and pasting code is equally do-able in both languages.
Kind of, Java 20 and C# 11 have lots of interesting material for job interviews and pub quizzes that I bet random joe/jane developers will fail at.
I am mindly aware of them, because alongside C++, they have been my main tools for the past 30 years (rounding to the oldes one), and I surely would fail as well.
I used Haskell for a few years and the main problem with it is that to do anything you need to use Monads, Functors and a lot of complex types. It's a requirement to even write the most basic program. "strange" functional stuff proliferates haskell like `foldl` and `foldr` and a extremely discouraging Lazy computation model that makes it almost impossible to know what the compiler will do unless you have a near perfect comprehension of all the language.
Rust to me is as simple as writing annotated Python or Typescript, with the only addition that a beginner needs to know is that there is a new option that those two languages don't have which is pass by reference or pass by value. The lifetime stuff was really easy to get, "pass a value into a function, can't use it anymore."
The Rust compiler gives extraordinarily useful error messages. I don't know if haskell improved but you can end up with some very strange stuff to figure out.
The only real hiccup I had when learning Rust was that going from `sync` to `async` code had a bit of a learning curve. It was simple for basic stuff, but the problem were that Future's have their own Lifetime that depends on every piece of data used in the computation between `await` calls. This means all your data needs to be Sync + Send if you want to use Async.
Separately, to express code which creates multiple non-const references into a single object (trivial in any other imperative or OOP language), you need compromises like verbose Cell, overhead RefCell, ugly and footgun-filled UnsafeCell and raw pointers, unsafe confusing (and currently unsound, see https://github.com/rust-lang/rust/issues/63818) Pin, or experimental ghost cells (https://plv.mpi-sws.org/rustbelt/ghostcell/paper.pdf, not sure the current implementations).
More and more, I find this is just a general problem and not a rust specific one. Any language with enough syntax flexibility will have a group of programmers that try to treat the language like lisp or haskell. (I recently ran into exactly this with a typescript lib I've been working with).
C++ is no stranger to this problem, it's famously the reason Java doesn't have operator overloads.
What I'm also finding is that complex style of programming is annoying to find in the company code base, but is rarely one that catches on in the ecosystem as a whole.
So multiple mutable references to the same data? I'm personally happy that's harder to do in Rust, as it enables anti-patterns like God Objects [1] and spooky action at a distance [2].
I don't like the "you're doing it wrong" response when people point out flaws, and that's not what I'm trying to say here. You can write perfectly fine code with remote mutation. This is just one example where the things that Rust chooses to make harder are also the things I have a harder time understanding.
I'll also agree that remote mutation is still necessary for some important use cases (e.g. testing, memoization). Then you have the `Cell` / `RefCell` escape hatches. Rust is pragmatic about such things.
> you need compromises like verbose Cell, overhead RefCell, ugly and footgun-filled UnsafeCell ...
The actual need for these constructs tends to be rare in my experience.
We have tens of thousands of lines of Rust in our codebase and ~five instances of these wrappers. They're really not hard to avoid. And actually, looking through the results here, I've found a few cases we actually _shouldn't_ be using `RefCell`.
> unsafe confusing (and currently unsound ...) Pin
This is a fair point. `Pin` always felt to me like a compromise to get async out the door. The best thing I can say is it's proof that the Rust team is willing to not let the perfect stand in the way of the useful.
But I'll concede it still is difficult to understand and complicates the async picture. This is definitely an example where Rust's underlying philosophy makes things that should be easy harder.
1. https://en.wikipedia.org/wiki/God_object 2. https://en.wikipedia.org/wiki/Action_at_a_distance_(computer...
Can you give an example of that? Do you mean something like, using .map() and .map_err() on Result?
I can't imagine myself not using those methods; they make programming much more enjoyable, and should be considered elementary as of 2023.
(Note: about .map_err() specifically, error conversion is kind of important in Rust; it's usually performed automatically by the question mark operator but sometimes you need to do it explicitly, and doing so without .map_err() usually obscures the intent of the program for no gain in simplicity.)
I know for myself, I overdid it at times as I was learning the trade offs.
Where I've currently settled is simple map/filter operations to tweak things into a more readable state are better than the more procedural approach. Also keep to simpler combinators (e.g. no `map_or` / `map_or_else`). If things become too complicated, be procedural. If there are side effects, be procedural.
Another way I have for framing it is the code layout (files, functions, blank lines, newlines, indentation) should be used to highlight the business logic. Early returns, `?`, post-fix combinators, etc should be used to reduce boilerplate that detracts from the business logic.
Yep! That's why Iterator::for_each https://doc.rust-lang.org/stable/std/iter/trait.Iterator.htm... is more often than not a code smell [0]; it exists mostly for the sake of writing side effects in functional style.
So something like
something.map(|x| ..).for_each(|x| do_something(x));
Should probably be written let iter = something.map(|x| ..);
for x in iter {
do_something(x);
}
It's still okay to use Iterator::map and other iterator combinators, but using for instead of for_each here is probably easier to read.[0] One exception is in the documentation "In some cases for_each may also be faster than a loop, because it will use internal iteration on adapters like Chain."
One of the benefits of Rust is that it gives you an option to switch to imperative style if thats what you choose which isn't the case for Haskell or purely functional languages.
Tacit programming was never a problem as the IDE does a pretty phenomenal job of telling you the types anyway. If the IDE can't figure out the type, the compiler won't either.
non-const references in a single object can simply be solved by making the whole object mutable, which is essentially what Python et al. do for you anyways.
I never had to use any unsafe code or pointers. Not sure if that way of thinking is carried over from C and C++ engineers. The only time I think I use Cell or the like is if I have a global static variable that I need to fill in at runtime. But that is more of a code-smell for most or at best a more advanced programming design that beginners don't need to worry about.
The best way to solve the last problem is to understand your data model from the start and make what objects need to be mutable mutable.
I think anyone who's done anything beyond beginner JavaScript is going to understand the difference between pass by value and pass by reference, even if they don't call them that?
function foo1( val : string ) { val = "hi" }
function foo2( val : [string] ) { val[0] = "hi" }
const bar : [string] = ["yo"];
foo1( bar[0] );
console.log( bar );
foo2( bar );
console.log( bar ); > I think anyone who's done anything beyond beginner JavaScript is going to understand the difference between pass by value and pass by reference, even if they don't call them that?
Pass-by-reference is a concept that is unfamiliar to many (most?) modern JS/TS developers, as the common idiom is that you "return" a new object or value at the end of your function.If you want to update a value in an existing object, you pass it by value and return a clone of it with the updated values
Performance doesn't matter, you can do the most pants-on-head-stupid thing you can imagine, or have your frontend page/component be re-rendering 20 times on every state change, and it makes no perceptible difference. (This has been my experience)
It wasn't until I learned C that I realized you might even program in this fashion (somewhat to my horror) where passing variables to functions could mutate them
If you pass an array to a function in JavaScript, then any changes you make to it will mutate the version the caller has.
For Safe Rust this feels pretty OK to me. You're much closer to the Java world where not understanding properly leads only to unnecessary performance overhead, than the C++ world where it's maybe going to catch fire for seemingly no reason if you misunderstood.
A Rust beginner is just as likely to make sub-optimal choices as a C++ beginner, but it's far less likely that to their astonishment their program explodes messily. Suppose I have a Bunch of Clowns, I want the six funniest Clowns, in both languages the beginner is likely to try to sort all the Clowns by how funny they are. But actually in both languages we can and (optimally) should tell the algorithm we only care about six, it needn't sort the 7th and subsequent Clowns at all.
But in Rust if the funniness of a Clown is a floating point type, the programmer is obliged to explain what they actually mean to happen here†. Floating point types can be NaN and we can't "just" sort NaNs as if somehow not-a-number is comparable when the whole point is that it isn't! In C++ the standard actually does say, deep in the ISO Document, that you can't sort NaNs, but you won't be warned about that it just silently makes your C++ program Ill-Formed.
† "It's OK, Clown funniness is never NaN" is a reasonable thing to believe but Rust will make you write that assumption into your program code.
1. Plenty of tools to keep track of your dependencies. It is also entirely possible to go it your own way and keep dependencies very low. You just have to implement more things yourself. Dependencies don't just insert themselves into your code on their own.
2. I don't even understand what you mean. Rust has a ton of very good resources to learn from coming from all knowledge levels. You can also use Rust at an entirely C level and only use as many advanced abstractions as you are comfortable with.
C++ in 1993 was also quite easy to learn.
Learning curve. Yes there is one. I dare to say lower than C++ though. Getting up snd running with Rust to do build clis, small web servers, python extensions, wasm for the web is not any more difficult than with C++. I think there is a lot to say for incremental learning with Rust. You can forget all about lifetimes (almost). You do need to deal with the borrow checker my in my experience developers get it. I think the Rust community has done a great deal of fantastic work on this "step learning curve". It will never be pythons, but if you are comparing to C++ then then... Hey, I think it might be easier!
But I haven't seen outright hostility in the community.
BTW, we had a discussion about monads in Rust forum, and I pointed at this same self-referential definition.
As for problem 1 (and I say this as a professional rust developer and shill), it's extremely serious and people in the ecosystem do not seem to care enough about solving it. You can steal login credentials and SSH keys by typosquatting today, and all a user needs to do is open an editor. I get why the solutions pitched are not implemented, but it's troubling that major companies are investing into the ecosystem and consuming from it but not securing it.
Software engineering != Computer science
Do you really think this or are you just projecting elitism?
Formal theory of computation (finite state automata etc.) was also a higher level elective that depended on your concentration.
The good:
* Cargo! It’s amazing, thank you.
* Portability. The build model is the same between different platforms; all the time I waste managing multiplatform builds with C++ instead goes to coding.
* Libraries. C++’s stdlib has gotten a lot better, but it still can’t parse json or make web requests or create zip files.
* Community. The vibe from docs and blogs and videos is good, cheerful and optimistic.
The bad:
* The borrow checker. Memory safety is great but there are plenty of memory-safe designs that the borrow checker complains about. With modern C++ and sound design techniques, memory safety is not something I worry about much in my C++ projects, and the static analyzer proves me out on that. In particular, a typical application design involves some variation of the Observer pattern (implementations can vary wildly), where views read/“observe” models owned by controllers. Why should I beat my head against the wall with a systems language? There should be an escape valve that doesn’t sacrifice memory safety.
* Documentation about the borrow checker and lifetimes. The Rust book tries to treat this in an approachable manner, but I just wind up more confused; I don’t think it can be treated casually and as such I’d like to have a very thorough and detailed description of what -exactly- is going on with these language elements. If Rust’s going to be a systems language, it’s going to need to get comfortable describing what’s happening precisely.
* dyn. I love templates, static polymorphism is great. But sometimes you need runtime polymorphism. Rust treats dyn like a second class citizen.
* Arc<Box<Foo>> ugliness. Heap-allocated reference counting is a useful thing sometimes. I know Rust doesn’t prefer it, but Rust’s FTFY attitude here is annoying.
* Where are the functors??!?!? Lambdas are great and all, but I can’t create unbound trait/struct functors and then invoke them on strict references later? This is a huge limitation in runtime flexibility for application development. Boost::function and boost::bind worked with VC6 twenty years ago! When I realized Rust didn’t have functors, I became much less interested in the language.
* No function overloading. This is just silly and onerous. Rust could create sensible restrictions to avoid ambiguities and C++’s ADL/type coercion complexity but there’s nothing ambiguous about having two functions with different arity sharing the same name. The hardest problem in CS is naming things, and Rust’s lack of function overloading makes it harder still.
In general, Rust seems a whole lot less expressive than C++, and the claims that these restrictions are sacrificed for memory safety are bogus. Nothing above involves pointers and reinterpret casting and void*.
... but rust cannot do this three things either from just the stdlib? I don't see anything related to requests, json, or zip files in here: https://doc.rust-lang.org/std/#modules
otherwise if you can install libraries, json, zip and web requests are just one vcpkg / conan dependency away in C++ too - and if you want to limit the amount of dependencies you can just use boost which in 2022 does support json and web requests, and can compress / decompress gzip (not zip though :/)
Don't you still have Iterator invalidation to keep track of and I thought I saw posts regretting using `std::string_view` because it is easy to reference deleted memory.
Anecdote: I maintain a template language library for Rust. I saw the potential for it to speed up if I used more borrowed data (like `&str`, Rust's version of `std::string_view`). I gave it a try and once it compiled, it just worked without crashes. I reflected back on if this was in C++ and the conclusion I came to was that maintainable code is a much higher priority than performance and that any future change in a similar C++ code base would require global analysis to make sure it was safe, making the performance gains not worth the lack of maintainability. In Rust, its been trivial.
> Where are the functors??!?!? Lambdas are great and all, but I can’t create unbound trait/struct functors and then invoke them on strict references later? This is a huge limitation in runtime flexibility for application development. Boost::function and boost::bind worked with VC6 twenty years ago! When I realized Rust didn’t have functors, I became much less interested in the language.
The fact that you can't `impl Fn` bothered me for a long time. It was one of the many examples of where Rust felt unfinished.
Recently, I've seen an inverted pattern for this, define a trait and `impl MyTrait for Fn`. I've found I much prefer this pattern over the cases where I would have used a functor. Granted, there are more ad-hoc cases where functors would be better.
> No function overloading. This is just silly and onerous. Rust could create sensible restrictions to avoid ambiguities and C++’s ADL/type coercion complexity but there’s nothing ambiguous about having two functions with different arity sharing the same name. The hardest problem in CS is naming things, and Rust’s lack of function overloading makes it harder still.
I thought I'd miss this but it hasn't been as bad as I expected.
I don't run into iterator invalidation that much in practice. I'm not often erasing elements from collections. There are all sorts of things like std::string_view where you could reference bad memory if you don't manage lifetimes, but I... just... come up with designs where lifetimes are managed properly? It's probably habit and very intuitive for me, hardly conscious of it.
Also, this is where the static analyzer is fantastic. If you have good unit test coverage, it will let you know you've screwed up and pinpoint where.
I don't mind the borrow checker as a default, but it would be nice for an escape hatch that wasn't an unsafe{} block. Perhaps one with runtime checks? Predictable conditions are virtually free with branch prediction.
> Recently, I've seen an inverted pattern for this, define a trait and `impl MyTrait for Fn`. I've found I much prefer this pattern over the cases where I would have used a functor. Granted, there are more ad-hoc cases where functors would be better.
Can you give an example or provide a citation to this? I'd love to read more.
It's the compsci version of Hawking's quote 'for every equation I include in this book, I'll lose half the readers' (or words to that effect). For every extra line of setup, you lose some of your potential audience.
Just setting up C++ tooling requires non-beginner level skills.
I've taught C++ beginners to use a C++ compiler, linker and make with no problems, and earned quite a bit of money while doing so.
But why would any beginner do not do the same thing that Rust offer: use one written by someone else? It does not have to be special or even open. Beginner needs one that they can learn from other people around them. Most of corporate ones are not even documented. People onboard by watching over a shoulder how it works xD.
I've certainly seen truly awful C++ codebases, full of undefined behavior and stack and heap corruption. It's possible to write very bad unsafe C++ code; it's also possible to write safe C++ code without much difficulty.
As far as design patterns: fair point that design patterns and idioms may be different in Rust but... what are they??? Fundamentally, in an application, some parts are going to read some data while another part owns it. How does that work in Rust? I haven't seen good answers yet. With procedural information processing utilities those concerns don't come into play as much, but I'd love to read about the architecture of well-functioning Rust GUI app.
The escape valve here that doesn't sacrifice memory safety is reference counting and perhaps interior mutability. (And this point certainly does involve pointers, if not void*.)
> I can’t create unbound trait/struct functors and then invoke them on strict references later?
You can't implement the `Fn` traits for your own types, but this shouldn't have any impact on what you can express, should it? If you're going to pass in references at the call site you can do that just as well with lambdas (or if you really want to use your own type, with a hand-rolled trait to replace `Fn`).
Not exactly sure what you mean about functors though. Maybe an example would help.
No function overloading was definitely a good decision. You can emulate variadic functions by implementing traits for tuples.
type ArcWrap<T> = Arc<Mutex<Whatever<T>>>
>Arc<Box<Foo>> ugliness
Box is not needed, Arc<Foo> should already be heap allocated.
But I feel you in that I'm an average-ish developer, and I write Rust with a constant fear that I'm "doing it the dumb/noob/wrong way". For example, maybe I just want to write a print method for an object, but I know the Right Way is to impl Display, so I do that so I won't be ridiculed.
I imagine the total time spent compiling Rust to be much, much lower.
This is one example but there are other threads of work where what they’re adding brings more consistency across the language, making it easier to learn.
Val is probably the next language that fleeing C++ developers should get behind.
There's even science behind it: https://www.jot.fm/issues/issue_2022_02/article2.pdf
https://en.wikipedia.org/wiki/Stranger_in_a_Strange_Land#Gro...
> Looking at my hypotheses, I was wrong on all counts
The author expected some things, and had good reasons to, then did a lot of serious work, and ended up finding out somewhat different things. Impressive and honest exploration of a complex topic that is quite hard to measure.
I wish more papers were written like this with clear separation of hypothesis, experimental results and conclusions. Also the clarity of presentation was superb again not an easy task.
Extra bonus points for the intellectual honesty.
There are two solutions to this:
Raise the FCLK (Fabric Clock) to 1900. This should be well tolerated by a Ryzen from the 5000 line.
Lower the RAM clock to 3600 and set sharp sub timings. This will take a little more time but this tool [0] will help. Runs on Windows though, i have an old partition around for stuff like this, dunno if a VM will do. RAM OC became a time consuming hobby for some people, the rabbit hole...
[0] https://www.techpowerup.com/download/ryzen-dram-calculator/
(edit) add sentence bout windows
Target CPU Speed: 3400MHz
Target DRAM Frequency: 800MHz
Target FCLK Frequency: 1800MHz
DRAM timings: 19/20-19-19-19-39
(The DRAM CAS# Latency setting is set to 19, but "CHA" and "CHB" show 20. I don't know what this means exactly.)
So I think you're right, scns. I was not getting the best performance.
---
I changed FCLK to 1900MHz and re-ran the Linux C++-vs-Rust benchmarks. Some results:
build+test w/o deps: 1847ms Rust, 1874ms C++ --> 1801ms Rust, 1837ms C++
incremental test-utf-8: 288ms Rust, 358ms C++ --> 280ms Rust, 350ms C++
So I did get a performance win for both C++ and Rust builds by fixing my FCLK. Thanks!
Why on earth is this a binary program?
Is there a document online which explains the maths or just implements it in javascript? I don't feel like spinning up a windows VM just to do this kind of calculation.
Hmm, I think I did this, or something like this. I'llcheck tomorrow. (If I did, then my comment about not overclocking the CPU is misleading!)
I believe the core of the problem is that it has to reparse the code to pattern match for the macro.
One egregious patter is tt-munchers [1] where your macro is implemented recursively, requiring it to reparse the source on each call [2].
In one of my projects, someone decided to wrap a lot of core functions in simple macros (ie nt tt-munchers) to simplify the signatures. Unlike most macros which are used occasionally and have small inputs, this was a lot of input. When I refactored the code, I suspect dropping the macros is the reason CI times were cut in half and a clean `cargo check` went from 3s to 0.5s.
[0]: https://nnethercote.github.io/2022/04/12/how-to-speed-up-the...
[1]: https://veykril.github.io/tlborm/decl-macros/patterns/tt-mun...
[2]: https://github.com/dtolnay/quote/blob/31c3be473d0457e29c4f47...
Some of our dev builds actually are split up into separate .so files which is a bit slower at execution time, but means linking is a lot faster, especially when you want full debug info...
I reported that here: https://lists.gnu.org/archive/html/help-make/2016-11/msg0000...
...and the maintainer said that the behaviour isn't intentional, but after a short look at the source code I decided fixing it was going to be beyond my abilities/motivation.
(I suppose this is more relevant to full builds, not incremental ones.)
In the old days it used to be quite common on UNIX world as well, no idea why "modern" Linux seems to have forgotten about this approach and always builds everything as a single project unit.
Gold and bfd are two examples.
I use these for development and then test with the default linker just in case there’s some subtle issue hiding somewhere.
lldb and mold (mentioned in article) are much faster.
As a project grows larger, link times become more noticeable, and mold starts to help.
[1] the full project, not the 17k SLOC subset discussed in the article
I thought the opposite: header files (and CMakeLists.txt, which was included in the subtotal SLOC) artificially increase C++ SLOC.
> For example idiomatic Rust code can do with less defensive programming due to language features.
Can you show an example in the Rust code where this might be possible? https://github.com/quick-lint/cpp-vs-rust/tree/master/rust
I know that the comparision here is a line by line comparison, but Rust gives a more stable interface for reusing libraries than C++.
It would be a great experiment to use lots of external libraries. You might find that the code doesn't get slower (build times would probably go up though )
How would Rust LinkedVector's implementation have fewer lines than C++'s linked_vector?
> It would be a great experiment to use lots of external libraries. You might find that the code doesn't get slower (build times would probably go up though )
Build times going up is a great reason why I would not use external libraries!
Also at least in VC++, modules are already a thing, no need to keep parsing template code all the time.
Doing a C++23 import std (which imports the whole standard library) takes a fraction of the time of #include <iostream> in VC++.
So a clean build out from repo means compiling only our own code, not the whole world as in Rust.
Still looking forward to binary libraries support in cargo (well one can dream).
You're right. My project is not one of those projects.
As mentioned in the article, the C++ project has no dependencies (outside googletest and the standard library, whose build time isn't included in the benchmarks).
Also, the project is relatively light on templates compared to most C++ code that I've seen. The biggest offenders are narrow_cast and vector.
> Also at least in VC++, modules are already a thing
I'd love to use them, but I mostly work on Linux and macOS. =[
> Still looking forward to binary libraries support in cargo (well one can dream).
I didn't consider this downside of Rust! I imagine rlibs can be cached manually, right? I don't know if rlibs are portable between machines.
Is this not what sccache is already doing?
It’ll be nice when this can be reliably used. According to cppreference MSVC has partial support and other compilers have zero support.
I’m not sure modules will be in widespread use before 2029.
GCC is getting there.
clang, well they seem stuck with their own initial modules implementation (module maps based), and now with Apple and Google out of the picture, the various compiler vendors that benefit from MIT license don't seem that eager to contribute to upstream other than LLVM.
Unfortunely everyone else seem to still be catching up with C++14 and C++17, depending on the ecosystem.
No, but the code I write for my job does have to be cross-platform from day 0. And that's the code base that is large enough where module improvements to compile time would be most useful! Hobby projects are small enough it's not super important.
So C++20 modules don't help me and probably won't until 2029! To my great dismay.
I don't understand how conan managed to get so much traction, it is so very bad, most of its recipes have billions of options most of which don't work.
The only sensible way to work with C++ is to always build from source, and let the build system transparently cache and reuse binary artifacts as an optimization.
Importing binary packages is a tradeoff.
Then again, it isn't something that is relevant in GNU/Linux world, only in other platforms.
You would still build your code from source, and be very careful handling any dependencies you don't have the source for.
The worst kind of undefined behaviour you can have in C++ is ODR violations, and importing binaries is more likely than not to trigger them, unless you closely manage their ABI.
Simply put: Rust doesn't offer an abstraction past type checking. Rust could potentially introduce its equivalent of PCH, or maybe it already does so in some internal cache, but as long as it uses monomorphization and no PhD student comes up with a clever trick to combine that with separate compilation it will scale worse.
That being said, maybe a more fair comparison wouldn't involve polymorphism in the rust code? C++ doesn't really have polymorphism, it uses templates for a similar feature. So what happens if someone replaces rusts polymorphic functions with macros?
The only thing missing was effort into the idea to make it the default and able to be supported long term. You need a stable WASM ABI that won’t change across rustc versions and for someone to add a runtime to rustc.
Dtolnay (the author of the Watt concept) recently put out a message looking for help putting together an RFC. https://twitter.com/davidtolnay/status/1574913813400330241
- Templates (compile time), which are generic bits of code that are monomorphized over every combination of template parameters that they're used with.
- Virtualization (runtime), classic OO style polymorphism with a VTable (I think this similar to Rust's dyn trait?).
I still think that C++ templates are different than rust macros as they are not a separate pass from type checking.
Also creating nested libs as separate compilation units is so difficult with cmake etc I rarely seen a large C++ project do this... it is much easier with Cargo.
I think using features of the language is fair game
btw, don't C++ templates also use monomorphization? I agree C++'s implementation is "lower level" (from a compiler perspective) than Rust generics, but that's one of Rust's selling features
If you do the same in rust, you need access to the module's implementation for monomorphization. You might skip the type checking of a function of the implementation or even cache the generated code for a monomorphization as optimizations, but generally you have to treat every function as often as it is used. So the best rust could do is akin to C++ precompiled headers whereas a language like OCaml can simply compile every module exactly once.
Note: OCaml pays for this elegance. Its uniform object representation is less efficient than what rust does. It's a trade-off that the rust designers consciously made, I think.
For full builds, C++ will take longer to compile than Rust (i.e. Rust wins).
This is because of C++'s #include feature and C++
templates, which need to be compiled once per .cpp file.
This compilation is done in parallel, but parallelism is
imperfect.
The author gives later on within the article a hint upon "and externing template instantiations". This allows to compile a template just once with a given set of template arguments. Shall reduce reduce compile-time. I assume it also reduces size of created object-files?Explicit instantiation of templates is supported since C++11. Therefore templates with one set of template arguments can be are declared in multiple files and defined just once - in one file. The keyword for this is extern upon each declaration. Please note, that all member of a template will be instantiated whether used or not. The user "strager" mentioned that briefly.
see:
https://en.cppreference.com/w/cpp/language/class_template#Cl...
https://en.cppreference.com/w/cpp/language/function_template...
Normal template instantiations are deduplicated by the linker. Total object file size is smaller with explicit template instantiations, but the size of the final executable is the same with both styles.
even with an incredibly powerful machine it takes many, many minutes to compile projects like chromium. On a "normal" machine, it takes several hours.
This is non-ideal and chromium is probably and already heavily optimized and well written project with lots of documentation.
Go is super duper fast, so there's that.
I want both. I was hoping Rust would give me both, but it doesn't.
If Chrome were written in Python it’d be hella slower iteration times
For small→medium projects I agree that Rust compile times are fine
In addition, I'd be interested in seeing how `cargo check` fares. In practice I do `cargo build` quite rarely, in favor of `cargo check` and `cargo test`.
On the hardware in question, which has loads of cores, I’m very confident that Rust would fare considerably better with 24 17.1k-line crates (410k lines, larger due to duplicating the entire thing rather than just the lexer) than with the one 104.4k-line crate apparently tested.
I didn't consider this in my scaling benchmark. You make a good point.
Also, I think it's a little silly to say that Rust build times scale poorly (even if it isn't quite as good as C++). 100k LOC in, what, 6s at the end there is not so shabby if you're typically never building all of it.
The benchmarks show Mold for both Rust and C++. This wasn't explicitly stated in the article; sorry. https://github.com/quick-lint/cpp-vs-rust/blob/f8d31341f5cac...
There are so many disparate build systems, none of them are intuitive, none of them do much in the way of error checking or give good user feedback, and the resulting build is always slow.
And as for the code itself, there's no type of linter or checker that will tell you if there's a way to save on compile time. "Hey, this could be a forward decl instead of a include!", or "Hey, this include is completely unused!" or some such. (There's include-what-you-use but I could not for the life of me figure out how to make it work)
I know the problems are hard, I just wish more effort was expended to make the whole process less painful and slow.
Did you try upgrading your hardware? It really helps.
Based on my profiling, it seems like the code gen backend (LLVM) is a big offender. A code generator optimized for build times would be nice. (Cranelift seems to be a failure in that regard.)
C++ has an explicit template instantiation feature which helps reduce backend time. I wonder if this can be done in Rust too. Maybe this is what -Zshare-generics=y is for?
And did you try any of the optimization strategies again at 24x? I would be curious if there are differences there.
Are you talking about run-time optimizations? My article is focused on build times. I don't think what you mentioned applies to build times. I could be wrong, though. (There are certainly compile time traps you can fall into, but I wouldn't know what those are in Rust.)
> And did you try any of the optimization strategies again at 24x? I would be curious if there are differences there.
No, I did not. That's a good suggestion. But I was pretty tired of this project by the time I made the scaling benchmarks. xD
Why can't Cargo parallelize rustc invocations per-file, like C++ can?
Why do I have to make the choice between faster compiles and faster binaries?
Compiling with and without optimizations is another example of that choice.
I don't want to base my project on an experimental language like Carbon or Val.
I want a C++ replacement which compiles quickly. Do Carbon or Val compile quickly? If not, I don't know why you mention these languages.
One example to get you started, https://github.com/pjmlp/RaytracingWeekend-CPP
On the other hand, that extra time is traded off against not having to deal with bugs at runtime that the type system is able to catch. Rust is one of the better languages when it comes to making illegal states unrepresentable.
But he does specifically break down how much of compilation time is spent in the borrow checker, LLVM, macro expansion, etc.
My C++ is rusty (no pun intended) but I struggle to imagine their variant of `vector.iter().map().collect()` to be as concise and fit in fewer than 4 lines.
I wonder if OP's C++ port doesn't use iterators that much, and how idiomatic it is.
EDIT: the code is not idiomatic at all.
I think I only used iterators in places where there's no built-in function on slices like C++'s strchr and strspn. (I think Rust's str has these, but not [u8].) For example:
C++: https://github.com/quick-lint/cpp-vs-rust/blob/f8d31341f5cac...
std::size_t length = std::strcspn(c, separators);
if (c[length] == '\0') {
return found_separator{.length = length,
.which_separator = static_cast<std::size_t>(-1)};
}
const char* separator = std::strchr(separators, c[length]);
Rust: https://github.com/quick-lint/cpp-vs-rust/blob/f8d31341f5cac... match s
.as_bytes()
.iter()
.position(|c: &u8| separators.contains(c))
{
None => FoundSeparator {
length: s.len(),
which_separator: INVALID_WHICH_SEPARATOR,
},
Some(length) => {
let found_separator: u8 = unsafe { *s.as_bytes().get_unchecked(length) };
match separators.iter().position(|c: &u8| *c == found_separator) {Syntax is usually `vector | map | collect`.
Not in C++'s standard library until C++20.
https://en.cppreference.com/w/cpp/ranges
Before C++20, similar functionality has been available in boost.
This is not my experience.
Lifetime and '&mut self' noise (and four-space indentation) did cause rustfmt to sometimes split function signatures across multiple lines, but overall, I think rustfmt did a good job.
C++: https://github.com/quick-lint/cpp-vs-rust/blob/f8d31341f5cac...
lexer::parsed_identifier lexer::parse_identifier(const char8* input,
identifier_kind kind) {
const char8* begin = input;
const char8* end = this->parse_identifier_fast_only(input);
if (*end == u8'\\' || (kind == identifier_kind::jsx && *end == u8'-') ||
!this->is_ascii_character(*end)) {
return this->parse_identifier_slow(end,
/*identifier_begin=*/begin, kind);
} else {
return parsed_identifier{
.after = end,
.normalized = make_string_view(begin, end),
.escape_sequences = {},
};
}
}
Rust: https://github.com/quick-lint/cpp-vs-rust/blob/f8d31341f5cac... fn parse_identifier(
&mut self,
input: *const u8,
kind: IdentifierKind,
) -> ParsedIdentifier<'alloc, 'code> {
let begin: *const u8 = input;
let end: *const u8 = self.parse_identifier_fast_only(input);
let end_c: u8 = unsafe { *end };
if end_c == b'\\'
|| (kind == IdentifierKind::JSX && end_c == b'-')
|| !is_ascii_code_unit(end_c)
{
self.parse_identifier_slow(end, /*identifier_begin=*/ begin, kind)
} else {
ParsedIdentifier {
after: end,
normalized: unsafe { slice_from_begin_end(begin, end) },
escape_sequences: None,
}
}
}Nitpick: The article compares Clang, not GCC. (GCC is mentioned only to show how much faster Clang is.)
> you get so much more static analysis by default in rust compilation than C++.
I care about the build-test cycle, so adding static analysis is against my goals.
I would minimize the analyses done by rustc if I could.
Nim doesn't look like the language for me.
https://vlang.io/#:~:text=Small%20and%20easy%20to%20build%20...
Edits: 64GB, not 64MB. 32GB is really the minimum recommended
I tested with two different machines:
* Linux machine with an AMD Ryzen 9 5950X
* macOS machine with an M1 Max
Each chart is labelled with 'Linux' or 'macOS'.
> If you want to compile a monolithic web application written in Rust within a reasonable amount of time, you need that much top-of-the-line hardware for your work. You're still waiting several minutes to compile, but compile times get even worse with older hardware running debian and mold/lld. CI test/build runs also take a long time to complete.
This is true with C++ as well. The article focused on the build-test development cycle, but you're right that the compile time issue would be worse on older hardware and CI.
And minutes? What are you compiling? How big is your codebase? Are you doing everything in docker or something?
Rustc is slow but I’ve never seen it be that bad. I’ve been working on a rust project for a couple of years, currently sitting at about 40k loc. Incremental debug builds still only take a second or two. Full compiles take about 10 seconds.
Indeed, 640KB should be more than enough.
No hard numbers, just anecdata.
Edit: now I think about it, the first time I heard the fan on the machine (out of ~3 times in ~2 years) was when I built a Rust project while running Airflow in docker compose under Rosetta emulation, back when one of the Docket images didn’t support arm64
The crates you import seem to add the vast majority of compile time stuff like an ORM brings in a lot of code but I still don't see this being an issue. It's so much faster than working on the React app and even that seems fine.
Unless you are working on some absolute mega project like chromium, compile time seems like a minor detail.
IF you have an M1 then sure. But bear in mind that’s 10x faster than the machines that some people are still using.
So, an Apple Newton in a laptop form factor?
The Mac M1 Max the author used is an absolute beast of a machine. I have a M1 Pro (which is not as fast) and it's ridiculously faster (and quieter - it barely needs a fan) than my Linux Dell XPS13 and older Macbook Pro.
[0] https://github.com/rust-lang/rust/pull/84762 [1] https://github.com/mozilla/sccache
I tested with rustc Git commit c7572670a1302f5c7e245d069200e22da9df0316, which (I think) includes that change.
> And for repeated clean + full build cycles there's sccache[1].
You're right. I included full builds in the article because almost-full builds happen a lot in C++ (after common certain header files, or if you think the build system broke something).
I imagine almost-full builds rarely happen when working in Rust though, so maybe I should have deemphasized my full-build benchmarks.
Partial builds are of course way faster, especially if you use many dependencies (i know you don't). I mainly work with the bevy game engine in Rust, which has a lot of dependencies. Even if i don't use its dylib feature, i get 2-3s compiles. And that's on a project with multiple hundreds of thousands LoC when you include dependencies. With dylib, it goes down to 0.5-1 second builds.
If your main conclusion is based on full builds, i would urge you to re-evaluate. The normal experience is just "cargo run" which rarely does a full build.
i.e., I type 'cargo build', it compiles the single .rs I changed almost instantly, but then I'm staring at:
Building [=======================> ] 314/315: landscape(bin)
for the next 20 seconds.
It's not. I show several charts comparing incremental builds too.
But 3 years later, many systems don't support them.
(This is mentioned in the article)
https://vector-of-bool.github.io/2019/01/27/modules-doa.html
https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2019/p14...
> Expectedly, as hardware parallelism increases, headers’ lead over modules becomes more and more pronounced. There is also a relationship between the DAG-depth (i.e. The length of the chain of modules that import each other). As this depth increases, modules grow slower and slower, while headers remain fairly constant for even “extreme” depths approaching 300).
In C++23 it takes less time to import the whole standard library than a single major include like iostreams.
Currently only available in VC++ preview, though.
For well organized large projects, modules would be marginally better at best.
I run `cargo clean` before each benchmark. That should clean up changes to third-party crates, right?
To put that in perspective, how easier it is to write a naive rust compiler than a naive C compiler.
Apparently not.
This is something Rust folks should strive to solve as a differentiator, because there is no realistic way of making incremental C++ builds faster.
I think C++20 modules are the promised golden ticket. It's 2023 and I still can't use them on Linux, though.
There's a chance that Rust is the programming language with the longest build times ever.
I worked on optimising lots of builds from Symbian to Android and other proprietary ones. C++ compiler choice is very important - gcc might seem slow but it is a speed demon compared to the old ARM compilers.
There are a lot of tricks based on trying not to do the same work twice like pre-compiled headers which might help massively (since headers can be much bigger than your code) but quite often these are also very fragile because changing one #define in one central header file might invalidate everything. It's not that easy to be sure that you are truly handling all dependencies. Hence one tends to have to regularly build from scratch to be certain that everything is done properly. If we really could handle it all perfectly then nobody would ever bother to build from clean.
It's also very important to be able to see how your build tool is scheduling tasks and making use of your CPUs - often some odd dependencies force most of your cores to sit idle while something gets done. There aren't good visualisation tools for this that aren't proprietary imo. I have cobbled together something for gmake which can output something you can visualise in chrome's profiler but it's crap compared to the best commercial tool I've used. You can remember the previous build times of various objects and use them to try to schedule large tasks as early as possible so they don't extend the build time by randomly getting started towards the end and then leaving the build going on and on just to get them finished.
The structure of your code matters too - a simple optimisation for C/C++ is just to have much larger source files rather than splitting everything up into many small ones. This acts to reduce the number of repeated includes.
If I was thinking about how to get fast builds it would force the language to change - header files would be banned and that means macros and the whole idea of preprocessing. That means a different approach to multi-platform support. Android benefits from having a java layer where you can pretty much build once and run anywhere and that is very good.
Eventually, on one big project, we got to the point where it was the packaging that was the slowest component and that was being done by the CI system .... for no good reason other than organisational inertia. Another team was responsible and didn't want to take advice. Since we couldn't bring it into the build and use all our tricks to optimise and redesign it we hit a wall where all our efforts to improve build performance had little impact.
The best tricks for improvement, of course, are the ones which always work and don't need a lot of maintenance to keep them functioning properly.
I think we need builds to be integrated with source control. Getting new versions of source and new binaries is all far too "separate" a process. Checking in 5 files should result in a very accurate rebuild of only the dependencies and a subsequent checkout should be able to get me those binaries. Developers should never really have to build all of Android just to work on some small part and they shouldn't really have to do more than one checkout to get everything they need including binaries.
Of course. I tried my best to optimize the C++ build and the Rust build.
> It's also very important to be able to see how your build tool is scheduling tasks and making use of your CPUs - often some odd dependencies force most of your cores to sit idle while something gets done.
This is a good point. I didn't spend much time profiling and tweaking this aspect of the Rust build (cargo build --timings).
> The structure of your code matters too - a simple optimisation for C/C++ is just to have much larger source files rather than splitting everything up into many small ones. This acts to reduce the number of repeated includes.
For clean builds, yes, this helps. But for incremental builds, where I need to test a change to just one .cpp file, larger source files are worse.
I think this is what Rust does. No header files; #[cfg] syntax for conditional compilation for either source files or sections of code. But it doesn't seem to fundamentally help build times. xD (But maybe the bottlenecks in Rust are elsewhere.)
The problem with header files for me is the increased fragility because a change in a -D option to the compiler can potentially invalidate all existing targets but it might not and you cannot easily predict if it will. There is also potential for changes in the filesystem between builds to cause different headers to be found. So e.g. if I download a build done by you to help me not have to build a whole OS from scratch, it might easily end up totally rebuilding when it doesn't need to or not rebuilding when it should.
So I think probably as you said - Rust is winning in one area and losing badly in another. It might, however, be more consistent than C++ and less fragile and that would be an interesting characteristic.
FWIW on Symbian and Android I never got linear scaling - beyond 16 cores the benefits of more cores tailed off rapidly. I tried a lot of things and never worked out truly why. We were scheduling lots of processes and keeping the cores busy but tasks just took longer. I wonder about memory bandwidth but really don't know.
You probably will. :-)
That... doesn't seem to be idiomatic Rust at all. unsafe and passing pointers everywhere? Reimplementing vectors and linked lists. Also, makes use of a ton of macros.
That's basically writing C++ in Rust.
Do you think this would impact Rust build times?
> Reimplementing vectors and linked lists.
Someone has to implement them.
Note that they're also implemented in the C++ code, so the comparison is fair.
> Also, makes use of a ton of macros.
Can you show an example where the Rust code uses "a ton of macros"? The only place I can think of is the tests, but I thought macros were what you're supposed to use in Rust for assertions.
If you're referring to proc macros, I thought those were very popular in Rust.
Perhaps, because the result is so un-idiomatic, it's unlikely to benefit from tweaks conceived for idiomatic Rust code. The Rust team uses Crater runs (re-compiling all of the public crates) to identify performance changes in the compiler, so if you do stuff that's far enough off the track there's no reason the compiler would get optimised around that.
Making your own vector seems like it'd be fairly up there.
> Someone has to implement them. Note that they're also implemented in the C++ code, so the comparison is fair.
I'm not sure I buy this argument in code like yours which almost invariably should just use the standard library and definitely needs to measure before replacing standard library features with your own hand-rolled equivalents. As I understand it your concern in this work was about development, and having a good standard library to lean on is a huge boon for development.
For example in strolling around this code, I ran into sorted_search. But, isn't this just [&str]::binary_search() except written by hand?
Actually the fact there are "linked lists" and yet there's no concurrency sets off alarms in my head. In highly concurrent software there are a bunch of clever lock-free algorithms for linked lists, so if you need that then you need it. But if you don't need this the linked list is usually going to be a mistake because on modern hardware its performance is abysmal.
Usually this is associated with C programmers, who don't know any better, but you've got other data structures, so, why Linked Lists ?
I did for the C++ code. And for a fair comparison, I ported my C++ vector to Rust.
If I made the C++ code use a custom vector and the Rust code use the standard vector, then people would complain that the Rust code was artificially shorter.
> For example in strolling around this code, I ran into sorted_search. But, isn't this just [&str]::binary_search() except written by hand?
Yes. But the standard binary_search isn't `const`, and mine is. I need to run my `sorted_search` at compile time to map translatable strings to magic numbers. (The C++ code does this too.)
> Actually the fact there are "linked lists" and yet there's no concurrency sets off alarms in my head. [...] why Linked Lists ?
The linked lists are actually linked lists of arrays. They are not grade-school linked lists with one item per node. In the C++ code, the classes are linked_bump_allocator (a memory arena) and linked_vector (a deque-style container).
I wrote linked_bump_allocator so I could use an arena allocator for parser ASTs, temporary strings, etc. I originally used Boost's monotonic allocator, but I wanted a rewind feature, so I wrote my own implementation.
I wrote linked_vector only because std::deque took too long to compile. (I'm not kidding.) So I could have easily used Rust's standard deque.
AFAIK they are very popular but also not recommended unless necessary, as they increase build times considerably.
All languages with macros tend to be abused, and the recommendation, whatever the language, is use as few macros as necessary.
The commenter you are so dismissively replying to is the author. He did not "give up only a little beyond something" like you did, he made a serious effort that probably took hundreds of hours, and documented it. I am not sure why you are posting such a comment full of disrespect for his work, after reading a couple of paragraphs of his article.
Is there really reason to believe that would be in advantage of Rust build times?
Ok, you say the proc macros you say.. but it also commented on, and also not unidiomatic?
I would expect an idiomatic implementation to drastically go down in lines of code... while compile times probably will go up.
Can you give an example of where something in the project could be, say, 20 lines of code instead of 30? https://github.com/quick-lint/cpp-vs-rust/tree/master/rust
It's also idiomatic for push to return nothing and for pop to return the item (if any - and not crash if not empty!). Similarly back should return an Option instead of crashing on empty (and is called last in Rust). for_each is also unidiomatic - it's much more useful to provide an iterator over the collection.
Here's an example implementation in ~60LOC instead of ~200: https://play.rust-lang.org/?version=nightly&mode=debug&editi...
(And further hint, that shouldn't be the significant hook here..)
////
Are compilation times a problem with Rust? Yes.
Are build times as bad with Rust as with C++? Yes.
Looking at my hypotheses, I was wrong on all counts:
The Rust port had more lines than the C++ version, not fewer.
For full builds, compared to Rust, C++ builds took about the same amount of time (17k SLOC) or took less time (100k+ SLOC), not longer.
For incremental builds, compared to C++, Rust builds were sometimes shorter and sometimes longer (17k SLOC) or much longer (100k+ SLOC), not always longer.
Similarly, Swift code is slow to compile for because it has a weird type system.
Sanitizers should primarily impact runtime performance anyways, but that's moot.
In my experience, ASAN has a huge negative impact on build times for C++ code (Clang and GCC).
If I was going to run sanitizers with the C++ code, for a fair comparison, I would also run the Rust code with Miri. (C++ sanitizers check unsafe code, but without Miri, rustc mostly checks only safe code.)
Monomorphized generics should always be written to hand off substantive work to a single function that can span multiple instantiations whenever possible, but this is not done to the fullest extent in current Rust. (Partly because Rust const generics are still at the MVP stage, so it's not possible to, e.g. write a generic function that depends on known features of a type, such as alignment or size.)
Looks like monomorphization part is not caught during cargo check according to https://github.com/rust-lang/rust/issues/49292
Reporter of the bug says:
> All of those happens somewhere inside librustc_mir, most of them being monomorphization. This corresponds to translation item collection pass, which takes nontrivial amount of time.
Which essentially means that cargo check is not doing all the checks that cargo build will do, so the comparison seems to be a bit off, at least for the time being. And consequently this inconsistency can easily lead to the hypothesis of LLVM backend being the bottleneck.
I guess the only reasonable way to know for sure where are the biggest bottlenecks in build times is to have something similar to clang's -ftime-trace but I couldn't find anything similar existing in Rust.
From what I understand, monomorphization and rust macros are essentially C++ templates in a nutshell, and probably less than, yet C++ is compiled much faster. Given that both clang and rust share the same LLVM backend, this seems like an indication to me that the bottleneck is rather in the frontend and not in the backend. It also could be that Rust frontend pipeline is not quite optimized yet so that it puts more pressure to LLVM backend than what the clang does but seems like we can't really know that for sure.
EDIT: Oops, I realized I already replied two hours ago.
C++ is a bloated mess that does exactly zero checking beyond type checking. Rust is a language that favors safety over build times and its serious users understand that.
Rust is also developed completely in the open. C++'s Standards committee is rife with resignation letters, accusations of misconduct, and walled gardens - not to mention years between standards updates, each costing a ton of money to even look at.
Yes, I understand. Rust has no standard. It has no stable ABI yet. Depending on who you are or what you're doing these may or may not be issues for you.
Alas, comparing these two as though one should outperform the other is nonsense to me. Of course Rust is slower than C++. C++ has had decades of maturity and has enjoyed many contributors to its tooling. Rust has not, and its feature set (as in, the things the compiler needs to do) is much broader than any C++ compiler is specified to do.
The end result here is this will only work to fuel more language wars and be used as a cheap weapon in further language debates. In my opinion, anyone who argued about which language is better than which other language has completely missed the point of computer science and programming.
My thinking was the opposite. Because Rust is newer, it doesn't have the bloaty baggage that C++ and its compilers have. And Rust's module system is (in theory) far better for build times than C++'s #include-based module system.
But instead of speculating, I put the time in to figure out the answer.
When I start a new programming project, I need to choose a language. For this project, three years ago, I had three serious choices:
* C * C++ * Rust
I rejected C because I wanted the productivity boost from C++ or Rust. I rejected Rust because Rust was new to me (thus risky) but I was very familiar with C++.
I don't see why it's a bad idea to compare languages—especially along specific dimensions, like run-time perf or build time or safety—when that's what us software engineers do whenever we start a new project.
Iteration time is an important part of the developer experience. The trade off of slower iteration time for improved safety is a real point that needs to be considered when choosing between C++ or Rust. This isn't something silly like curly braces vs whitespace.
Quite the contrary. The article is as objective and specific as it gets and makes no value judgements but uses well reasoned measurements.
The encouraged response to an article like this would not be "language wars" but doing related measurements, perhaps comparing the compilation speeds of idiomatic rewrites of existing tools, discussing the reasons for (slow) compilation speeds, and future improvements thereof.
See something a little out of whack here?