Very thorough write-up, and an objective comparison.
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.
I appreciate your comments, but I think the things you worry about aren’t that bad in case of Rust.
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.
> Not every language evolution is a cautionary tale. Java, C#, PHP, JS have grown a lot and are fine.
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.
Small crates is not a benefit wrt. The wrong 3 lines of code in crate is all it takes to be owned, regardless of whether the crate is 100 lines or 100k lines.
Large number of crates and developers is a negative, because one needs to trust or review all of them.
I don't really understand this line of thinking. The C++ method of "avoiding" this problem is to shove things into mega-libraries like Boost, or copying and pasting code. If you need the functionality, would you rather review 5 focused libraries, or Boost?
Copying and pasting code is equally do-able in both languages.
Boost -- it has a development process and has a proven track record. Those five focused libraries, times 30 other deps becomes 150 different developers to possibly vet. Anytime any of them can pass on the project to someone less scrupulous.
You should also keep in mind that a lot of Rust library developers release many individual crates from within the same project, inflating the crate count somewhat.
> Java, C#, PHP, JS have grown a lot and are fine.
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.
Can you give an example for #2? I picked up Rust this year and never found that to be the case. In fact Rusts type system is extremely clear to me and was pretty straight forward coming from a Python world. I don't think I ever encountered anybody quoting Category theory at me.
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.
Some Rust programmers, when faced with performing operations on a sum type (like Result) or container, tend to reach for point-free map-filter-reduce code stuffed with higher-order functions, lambdas, trait generics, and return type inference, requiring looking up the documentation or implementation of multiple helper microfunctions, and asking the compiler for the inferred return type. My preference is usually to write imperative code matching over objects or looping over iterators, except in cases like "minimum of list" where the loss of clarity of a loop exceeds the loss of clarity of a reduce() method call.
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).
> Some Rust programmers, when faced with performing operations on a sum type (like Result) or container, tend to reach for point-free map-filter-reduce code stuffed with higher-order functions, lambdas, trait generics, and return type inference, requiring looking up the documentation or implementation of multiple helper microfunctions, and asking the compiler for the inferred return type. My preference is usually to write imperative code matching over objects or looping over iterators, except in cases like "minimum of list" where the loss of clarity of a loop exceeds the loss of clarity of a reduce() method call.
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.
> Separately, to express code which creates multiple non-const references into a single object (trivial in any other imperative or OOP language),
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.
> Some Rust programmers, when faced with performing operations on a sum type (like Result) or container, tend to reach for point-free map-filter-reduce code stuffed with higher-order functions, lambdas, trait generics, and return type inference, requiring looking up the documentation or implementation of multiple helper microfunctions, and asking the compiler for the inferred return type
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.)
> Some Rust programmers, when faced with performing operations on a sum type (like Result) or container, tend to reach for point-free map-filter-reduce code stuffed with higher-order functions, lambdas, trait generics, and return type inference, requiring looking up the documentation or implementation of multiple helper microfunctions, and asking the compiler for the inferred return type. My preference is usually to write imperative code matching over objects or looping over iterators, except in cases like "minimum of list" where the loss of clarity of a loop exceeds the loss of clarity of a reduce() method call.
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.
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."
To be fair, this kind of thing even happens in JavaScript. Some use functional programming with immutable datastructures, higher order functions, super generic code, using some niche FP library with its own idiosyncrasies - when simple and imperative would do just great.
YMMV but I personally have never faced either problem having written tens of thousands of lines of Rust at this point. But to be honest I never worked on a large team of Rust engineers.
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.
> 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
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 );
The big difference when I use Rust is that in Rust every variable you accept in a function requires you to make that choice. Whereas JS it’s kind of chosen for you. In your example you had to go out of your way to do it. So just a bit more mental overhead for implementing libraries. But not a huge deal.
> 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
> 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.
It is not just the abstract knowledge of the difference, but the fact that "let b = a" makes a undefined and, especially, some comfort with a Rust-ic way of dealing with this. My 2c.
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.
well, ok but isn't it more likely that we want the clowns sorted in funniest order? and if we actually unlikely event do need a collection of the top six, we can write a collection to do that.
Suppose I have a million clowns. Even if I want the funniest six in funniest order the optimal way to do that is to ask first to partition just the six funniest, and then sort those six finally. Sorting the other 999_994 clowns is pointless for this task, and will waste the vast majority of our run time.
so, as i said, possibly write a specialised container to do that. but you are still going to need to read the million clowns, so maybe (i haven't investigated this) the best approach is to read the clowns, which you have to do anyway, and then see if they can be inserted into an array of the currently highest funniest (though i would never see clowns as being funny). i don't remember saying anything about sorting to solve this specific problem, only in the general case - perhaps i did not make this clear.
0. Learning curve is not even remotely close to C++ and all it's gotchas. And it will never be so. What has to be explained in every one of these threads, Rust is becoming EASIER to use with time not more. New releases make the std nicer and they remove language limitations.
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.
I recall that each revision of C++ is there to make the std nicer and remove language limitations. The problem is always backwards compatibility. What makes Rust different?
Another 2cents from another average everyday software developer that has been lucky enough to work with Rust professionally for the last two years:
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!
As a Rust learner (for 1 year already), I can confirm that these are legitimate worries. Learning curve can turn many away, and supply chain can be attacked as well (I haven't met any mentions of security checks of crate updates to prevent them). #2 is, I think, part of #0: when you learn the language you meet a good deal of new terms -- though not acronyms -- they're significant, but you're not yet able to gasp all of them, it takes some time.
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.
0 is real and people discount it a bit too much, 2 is kind of imagined. Every language has its jargon that is alien (for example in C++ RAII, SFINAE, "rule of 5" "move constructor" etc). Rust does borrow some language from other ecosystems but I don't think it's fair to say it's a problem or used to "project a coolness level." Jargon is used to describe specific technical details, it always sucks but it's also necessary. If you don't understand it then circle back to the 0th problem, which is the learning curve.
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.
I can understand that category theory is alien to most developers, but lambda calculus? Isn't it similarly basic knowledge in CS as Turing machines, finite state automata etc?
Nope, I'd say 90% of developers globally would not know lambda calculus. And in non-functional languages when was the last time you had to consider the differences between Beta and Eta conversions/reductions when writing a Django SaaS? Or a Java Swing GUI?
i wouldn't say that lambda calculus or turing machines have ever been part of my programming toolkit (i do know what they are), and i wouldn't expect say a new programming hire to know much at all about them. FSAs are a bit more useful, but you can get an awful lot of development work done without direct knowledge of them.
I’m a pretty good C++ developer and have been trying to use Rust for a new project.
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*.
> Libraries. C++’s stdlib has gotten a lot better, but it still can’t parse json or make web requests or create zip files.
... 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 :/)
> * 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),
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.
> 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.
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.
> Can you give an example or provide a citation to this?
It would look something like this:
trait MyOperation {
fn run(&mut self, a: T, b: U, c: V) -> R;
}
impl<F> MyOperation for F where F: FnMut(T, U, V) -> R {
fn run(&mut self, a: T, b: U, c: V) -> R { self(a, b, c) }
}
fn my_api<F: MyOperation>(f: F) { ... f.run(a, b, c) ... }
struct MyFunctor { ... }
impl MyOperation for MyFunctor { ... }
my_api(MyFunctor { ... });
That is, when defining `my_api` and you would like to accept both lambdas and functors, instead of using a `Fn` trait bound directly, use a more specific trait which has a blanket impl for lambdas.
> Can you give an example or provide a citation to this? I'd love to read more.
`nom`'s `Parser` trait is implemented for functions. You can mix and match parser functions and parser types, including the built-in types that are returned when you call the post-fix combinators on the `Parser` trait.
I've said many times that C++'s tooling is a dumpster fire of epic proportions. No-one that wants to use the language should have to spend a non-trivial amount of time a) downloading a build system in addition to the compiler and linker, and b) reading the docs of the build system they've chosen.
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.
You are right. I have spent a lot of time tinkering with build systems on sizeable projects to be sure I do not want to do it myself.
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'm by no means an expert, but I don't see a lot of your "bad" points, they mostly seem like gripes about not being able to use Rust exactly as you are used to in C++ as opposed to pitfalls of learning the Rust way and doing it with Rust patterns, which would be true any time you move from one language to another.
This is a pretty common complaint from people who have used C++ forever and don't want to give up their design patterns when learning a new language. "Not expressive" is self-serving way to say alternatively "I can't write my OO soup" or "I don't care about safety in my C++ code", which are both evidenced in the OP's comment.
I care about safety in my C++ code, thanks. What I'm saying is that all of the boogeyman stories about terrible C++ code generally don't apply to modern C++ if you work within its idioms -- memory corruption bugs are few and far between, and easy to find and fix.
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.
> What I'm saying is that all of the boogeyman stories about terrible C++ code generally don't apply to modern C++ if you work within its idioms -- memory corruption bugs are few and far between, and easy to find and fix.
I think people are increasingly frustrated by this refrain, because there's empirical evidence that suggests otherwise. Now, it's entirely possible that you are starting greenfield projects with only super senior engineers in domains where a few safety critical bugs just don't matter, and in that case the learning curve required for memory safety just isn't worth it. That's a business decision you can make, and I wouldn't judge for a second. There are always trade-offs in engineering. However, "I don't write bugs" just isn't credible in 2023. I'm sorry if this seems uncharitable, and I appreciate that you're making a more qualified claim, but safety is not a "boogeyman."
> I'd love to read about the architecture of well-functioning Rust GUI app.
GUI is certainly a still developing area in Rust and has a long way to go. It's easily the weakest part of the ecosystem. That being said, it's still totally possible to build GUIs in Rust. The most common architectural pattern would be the Elm architecture, which avoids many of the issues of ownership that more traditional OO patterns introduce. I've also had a lot of success recently using immediate mode libraries, although obviously this won't work for every application.
> 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.
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`).
I would add that I/O in Haskell is pretty sub-optimal, so that hinders its adoption.
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.
When you say I/O in Haskell is sub-optimal, are you referring to performance, or the fact that it can be cumbersome? I personally love that Haskell's type system tells you whether a function has any side effects, which of course includes I/O.
Mostly objective, but slightly misleading. I'd be interested in counting how many times a developer runs `cargo build` compared to `make` and `cargo check` compared to `I don't think there is a c++ equivalent`.
I imagine the total time spent compiling Rust to be much, much lower.
Adding things to Rust needn’t increase the complexity. One of the features that should arrive in a year or so is the ability to use async functions everywhere, even in traits. Right now there’s additional complexity and difficulty for beginners to know that “ok, async only in free functions and import the crate async-trait if necessary”. Future learners of Rust will simply use async wherever.
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.
Well, for languages that are now touting interop with C++, and the emergence of a mainstream memory safe language like Rust, Carbon seems like a retrograde step to me.
Val is probably the next language that fleeing C++ developers should get behind.
Very well written, and I like that it ends on this note:
> 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.
Totally agree. I would like to stress the "serious work" point. He tried a lot of configurations and kept track of the results. This is not easy as it sounds since most people lack the discipline (me included).
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.
Writing a blog post like this and running benchmarks on several combinations of OS, language and compiler options and different tools takes so much effort and time. We should be very thankful to the people who do this. I've done it a couple of times myself... but it was so much more effort than I thought, literally weeks non-stop, that I don't think I will be doing it again any time soon.
Thank you for your kind words! I appreciate your empathy. This project took about 7 weeks (including trimming, porting, benchmarking, optimizing, and finally writing the article).
An OT hint to the author: I noticed that your RAM runs at 3800, that leads to a mismatch with the Infinity Fabrics' clock of 1800, i.e. increased latency.
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...
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.
A surprising source of slow compile times can be declarative macros in Rust [0].
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.
In my experience of building large C++ projects on machines with >= 64 threads (especially if using ccache or something), linking is often the bottleneck to incremental compilation as opposed to cpu-bound compilation.
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 found that Ninja tries hard to build things in the order they’re listed in the build file (dependencies permitting). Whereas GNU Make... well, it does start off trying to build depth-first, but if it needs to build a target's prerequisites, it pushes that target to the very end of the queue. So Make ends up building the leaf targets at the very end -- it leaves all the linking steps until all the object files have been compiled, instead of linking each executable as soon as its own dependencies are available.
...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.)
Chrome is doing the exact same thing for the same reason. Dev builds are dynamically linked while release builds are statically linked. Works quite well, it's a great technique.
It is quite common in Windows, either produce lib or dlls for each set of main modules.
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.
I didn't get why he doesn't see barely any improvement using mold, while mold's advertising/benchmarks show 1-2 magnitudes of improvements over gold, and still at least 50% vs llvm.
Reading through mold's nice docs I could only explain that by him not utilizing multiple cores (molds actual strength besides other things), but seems pretty unlikely. Who can explain?
For the 17k SLOC project, link times were already under 100 milliseconds without mold. (EDIT: Actually linking took 129 milliseconds with GNU ld.) There are other, bigger bottlenecks to deal with.
As a project grows larger, link times become more noticeable, and mold starts to help.
On Linux, the default visibility is public (on Windows I think it's private). How much of an effect would the default visibility have when linking if most of the symbols in Rust will be kept private?
I don’t think the Rust code could be shorter than the C++ code when converting pretty much line by line, since that leads to C++-style Rust code. For example idiomatic Rust code can do with less defensive programming due to language features.
Just wanted to say thank you for sharing your code!
Im not familiar with C++, so Im discovering the union, UnsafeCell, ManuallyDrop and all those concepts that are rare in the Rust projects I see everyday.
Im learning a lot.
I just randomly looked at one part: linked vector.
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 )
> I just randomly looked at one part: linked vector.
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!
Thing is, not every C++ project is full of template metaprogramming, and at least on the corporate world it is quite common to use binary libraries, which NuGET, conan, vcpkg, and OS package managers support.
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).
> not every C++ project is full of template metaprogramming
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.
VC++ support is good enough that all my C++ hobby coding is done with modules, not everything we code must be cross-platform from the get go.
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.
> not everything we code must be cross-platform from the get go.
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.
> everyone that only depends of Windows and XBox workloads can also make use of modules.
So that covers projects funded by Microsoft and… almost nothing else. Because if you can ship on Xbox you’d probably be smart to ship on PlayStation and possibly Switch.
The number of real projects that can use modules isn’t zero. But it’s pretty god damn close! This ain’t the hill to die on.
I’m not an FOSS folk. I’ve shipped a lot of games. I spend 95% of my dev time on Windows.
> many studios actually enjoy the perks that come with being platform exclusives
Yes, building a game for a single fixed piece of hardware is great fun. But we live in the cross-platform age. As practical matter the Xbox One and Xbox Series Blah are losers. Shipping for Xbox but not Playstation (and often times Switch) is just leaving money on the table.
The list of “games released only on Windows and Xbox” is very small.
Importing binary packages is a terrible idea, and a source of huge problems.
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.
There is nothing special about GNU/Linux is that respect.
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.
I have a hunch that rusts global compilation model (as opposed to C++ separate compilation, as crippled as it is in practice due to header-only libs and link times) should always lead to C++ scaling better with the size of the project.
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?
This is pure speculation, but I suspect egregious use of proc macros in Rust also inflate compilation times a bit. Just speaking anecdotally, but removing proc macros in one of my hobby projects decreased clean build by about ten seconds. Definitely not a couple magnitudes like the author saw, but not unnoticeable either.
Proc macros are used too often in the Rust ecosystem, even for very simple crates. Library authors and maintainers care too little about compilation times.
I always wonder: If the proc macros were executed JIT-like with the Cranelift backend instead of compiling them with the LLVM backend, wouldn't that drastically reduce build times of proc-macro-using code? The Rust compiler spends most of its time in LLVM, if I recall correctly.
The most promising proposal so far is to compile proc macros to WASM. Then you precompile them to a single cross-platform WASM binary, publish those to crates.io alongside the source code, and then execute them in a sandbox. You wouldn’t see much benefit until all your chosen proc macros were available in this form, but after a while this would be great, basically no downsides. In fact you can publish proc macros this way today, IIRC with some caveats about compiler versions (?), but it’s manual and a proof of concept only so nobody has done it.
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.
Someone recently looked into doing this by default. The problem was the results varied depending on how often your "host dependencies" (proc macro, build.rs) were shared with your "target dependencies" (`dependencies`, `dev-dependencies`). When there was a lot of sharing, it hurt, but otherwise it sped up build times.
Mine were mostly third party crates like bitflags and thiserror. Not saying that's what was effecting your build times, I'm not really familiar with your repo (my comment was only directed at my proj), but also it's pretty hard to write a significantly large project without macros; it's just the devil you kinda have to live with.
- 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 think they meant to say that neither templates (nor virtual classes) are quite up to the same level of polymorphism that Rust has. Ie Rust type checkes at definition, as opposed to copy-paste and hope for the best (wrgt templates) when trying to compile the code. In other words Cpp templates are closer to Rusts declarative macros than to Rust generics. Hopefully concepts make this better in Cpp.
As always in C++ world, there is a workaround with static analysis, which could give errors when template code reaches out to capabilities that aren't part of the concept definition.
Does such a static analysis exist? I think the most realistic solution is to write an archetype class for each concept and instantiate each template with its relevant archetypes.
Now can we automate writing archetypes from concepts (and viceversa)? Maybe in C++64 when we finally get static reflection.
I really hate how C++/WinRT used the "C++ reflection is around the corner" excuse to kill C++/CX and downgrade the COM development experience back to pre-.NET days.
Apparently it isn't around the corner.
However with type traits and a bit of constexpr if (requires {....}) it is possible to have a kind of poor man's compile time reflection.
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.
Creating nested libs in cmake is trivial and is done all the time.
I'm sorry I was a bit snarky. But the thing is that the Rust evangelists don't understand the domains and types of large scale projects that C++ is used in and how those systems are built. Yet the they come along and keep pushing for Rust without understanding why C++ developers aren't switching and wave their hands at the issues we have with Rust.
Show me a successful multi million line Rust project. Maybe throw in a complex UI. Maybe some GPU computation.
> maybe a more fair comparison wouldn't involve polymorphism in the rust code?
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
Template specialization sometimes looks like monomorphization, but it is really a different thing. I would compare C++ templates more macros, as they tend to do way more (e.g., allow for overload resolution).
Please elaborate. I agree C++ templates are more flexible in what they do (SFINAE etc.), but Rust generics and C++ templates still both use monomorphization
I wouldn't say that C++ templates use monomorphization, because there are no polymorphic functions to begin with. Monomorphization, as in "the standard FPL technique", comes after type checking. C++ template specialization precedes type checking.
Of course one can redefine the meaning of "polymorphism" and "monomorphization" to fit C++, but then these words lose nearly all meaning.
In my plebeian brain I just thought “monomorphization = static dispatch” / 0-cost abstraction (vs. just writing multiple versions of a function), which both Rust generics and C++ templates fall into.
Now I see that “monomorphization” has a specific definition in FPL
I'm not satisfied by your explanation here. What definition of 'polymorphism' are you using? Why would type checking need to precede monomorphisation? Additionally, how do C++ concepts fit into your beliefs?
Note that it's called "parametric" because OO folks stole the word polymorphism for a completely different thing.
The idea of a polymorphic function is that it acts the same for all possible input values (for at least one parameter). This is captured by its type. If you look at a polymorphic function (say "f(x) = x" with the type "for all 'a. 'a -> 'a ") you can check that type once and you know that monomorphization works for any possible input.
Monomorphization is then the process to replace 'a with a concrete type to facilitate code generation. What it actually does, depends on your language of course. It might for instance decide how to pass arguments or compute offsets from record labels. Crucially, monomorphization in this sense never fails, at least not because you input the wrong type. The user doesn't need to know that monomorphization happens at all. It's an internal design choice of the compiler.
If you look at C++, template specialization is very visible. The template that represents the identity function doesn't have a type. Only after specialization, the user learns whether it was successful or not (e.g., because copying the type is not possible).
I never worked with concepts myself, but to my understanding, they are an attempt to generate more useful error messages. As far as I know, the concept declaration of a template might simply be wrong, there's no automatic check that guarantees that a template can be instantiated if I adhere to the contract, right? So the type check still has to happen after specialization and the user has to consider specialization in their mental model of the code.
"Beyond" in the sense of "after". Languages that support separate compilation usually have some sort of artifacts that do not need to be type-checked anymore. Say, you compile an Ocaml module. The result is a compiled (bytecode or native code) file that represents the implementation of the module and doesn't need to be generated ever again. Essentially, the compiler abstracts over a module after it dealt with it. You could the
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.
What you've said here is mostly correct. The key part you've omitted is that monomorphization is opt-in. One could write Rust like C if they wanted to; they would have to be insane, but they could
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.
> I assume it also reduces size of created object-files?
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.
This is a nice article. But, I don't really understand the conclusion that Rust compile times are a problem (and that C++ times are too). Rust is not a tool I pick up because 3s compile times are a nonstarter and I need sub 1s to feed my developer ADAH. I spend orders of magnitude more time writing Rust than I do compiling it so a 2s difference is utterly marginal. I don't know.. this obsession with compile times in the last 5 years seems rather irrelevant to me, not that I don't appreciate lower ones. But "problematic" is not an adjective I'd use.
In what language or toolset can you compile a project like Chromium faster? Big projects take a while to compile. Nobody has broken this rule yet so why are we fixated on it being problematic? Obviously, we could not compile the project and eschew all the optimizations we get, but then the end user would be unhappy. I don't know, I've always found the tradeoff to be quite understandable and inoffensive: if I want faster development (among other things), I use an interpreted scripting language. If I want a fast end product (among other things) I compile and optimize to machine code beforehand (obviously it's not that simple, but you get the point).
> if I want faster development (among other things), I use an interpreted scripting language. If I want a fast end product (among other things) I compile and optimize to machine code beforehand
I want both. I was hoping Rust would give me both, but it doesn't.
I'm curious if the results at the very end for the copy-pasted code benchmarks are a result of linking. They seem to have chosen to use mold for C++ and not for Rust after seeing that it gave little benefit for small projects, but I would expect that to change as the project scales.
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`.
I think https://github.com/quick-lint/cpp-vs-rust/blob/f8d31341f5cac... indicates Rust is using Mold too. And I get the impression that the thing unit being benchmarked is a single crate—that is, one crate 8×, 16×, 24× the size (which puts the burden on the compiler, in areas that don’t parallelise well), rather than 8×, 16×, 24× as many crates (which puts the weight on the linker and on well-parallelised parts of the compiler).
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’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.
Yea, comparing C++ build times and Rust build times seems to ignore the practical workflow differences between writing the two.
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.
> They seem to have chosen to use mold for C++ and not for Rust after seeing that it gave little benefit for small projects, but I would expect that to change as the project scales.
C++ build systems and compilation time in general is basically my most significant complaint as a C++ developer.
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.
I have found Clang's -ftime-trace flag helpful in finding bloated #include-s and templates. Also, I have analyzed the .ninja_log file (for CMake+Ninja) to find slow-to-compile .cpp files.
As a Scala developer, I can only dream about these build times. 1.8s to build a 17K LOC project .. This would take minutes for my similarly sized Scala project.
This looks completely out of the ordinary. With a warm compiler, on a 4 year old Macbook Pro, I get 2000-4000 lines/sec. I.e. 4-8 seconds for your project. Unless you do some very involved typelevel or meta-programming stuff that's what you should expect to see.
Yes my project does contain a bit of typelevel stuff (Cats Effect). And it's Scala 2.13, so not the latest 3.x which might compile a bit faster.
Also to be fair, I was referring to the time taken by sbt compile, which I suppose does more than just invoking scalac.
But still the time reported by sbt's Compile/ Compile incremental is over 200s.
I guess my question is - what is to be done? Even C++ is quite slow to compile. If all rustc development stopped and there was a years-long effort to write it "fast", what sort of improvements could we see?
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?
I don't know enough C++ or Rust to judge but is it possible that they are optimized around Rust that isn't C++ style? I know I have seen articles about other languages where some very minor changes make a huge difference because the optimizations in place didn't understand what was happening and had to work harder.
And did you try any of the optimization strategies again at 24x? I would be curious if there are differences there.
> I don't know enough C++ or Rust to judge but is it possible that they are optimized around Rust that isn't C++ style? I know I have seen articles about other languages where some very minor changes make a huge difference because the optimizations in place didn't understand what was happening and had to work harder.
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
I did actually mean build time. I figured there could be optimizations within the build process which allows some specific style of code to build faster because it has to check things. Perhaps around memory safety, which could be a big deal in the line for line translation.
And I totally understand being tired of it by that point. You did an extremely detailed and well documented process, and it was quite impressive. Thank you for sharing it.
Good question. The author of Zig claims that dev build times will be amazing. One technique is to avoid LLVM. Another technique is to integrate the linker. But all of that is a work-in-progress project.
> In theory, if you split your code into multiple crates, Cargo can parallelize rustc invocations. Because I have a 32-thread CPU on my Linux machine, and a 10-thread CPU on my macOS machine, I expect unlocking parallelization to reduce build times.
Why can't Cargo parallelize rustc invocations per-file, like C++ can?
The exact same thing happens in C++—when you're not using LTO, you can pass multiple source files to the compiler at a time to allow these files to be optimized together, but keeping them separated at a file-level prevents optimizations such as inlining functions between them.
Compiling with and without optimizations is another example of that choice.
You should definitely keep an eye on Val and Carbon. They're being designed to be interoperable with C++. Both are designed to match C++'s performance while still being able to work with your existing C++ code. Both of these languages offer a more modern developer experience and are built with software and language evolution in mind. They have practical safety and testing mechanisms, etc. Definitely check them out if you're looking for a C++ replacement!
I think it’s acceptable for Rust compilation to end up slower than C++. After all, the compiler does a lot more work for you with the borrow checker and all. That doesn’t mean we shouldn’t strive to make it better though.
It’s not because of the borrow checker specifically, but the type system design that makes the borrow checker possible is a big part of what leads to slower compilation times.
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.
Regarding the sloc count, the default automated Rust formating tool is very eager to adds lot of lines by basically keeping only one word per line.
Something I'm not a fan of, I must say.
It usually does that on iterator chains, which AFAIK do not exist as such in C++, so multiple operations would be expressed as multiple imperative statements.
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.
> I wonder if OP's C++ port doesn't use iterators that much, and how idiomatic it is.
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:
> It usually does that on iterator chains, which AFAIK do not exist as such in C++, so multiple operations would be expressed as multiple imperative statements.
> the default automated Rust formating tool is very eager to adds lot of lines by basically keeping only one word per line.
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.
An interesting thought is that we really ought to not compare rust with just GCC, but GCC + clang-tidy, since you get so much more static analysis by default in rust compilation than C++.
Kudos. Are there languages on the horizon that could also be included as possible serious choices? Nim, zig, jai, or we all will be eternally waiting for future version of C++ ?
Note how the OP uses an M1 laptop w/64MB of ram. 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.
Edits: 64GB, not 64MB. 32GB is really the minimum recommended
> Note how the OP uses an M1 laptop w/64MB of ram.
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.
I write web applications in C++. I definitely do not wait "several minutes" to compile. If it is a change in single CPP file it compiles under a second on my AMD 3950X desktop. Full rebuild takes 16 seconds (it includes compiling some libs I use).
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.
32GB ram is a minimum, not 64. No docker for local dev. If you find this unrelatable, you're not working on a monolith web application using dozens of dependencies and utilizing macros to the extent that one would be if they were using sqlx and other libs.
I’ve used a first-gen M1 MacBook Pro with 16GB of RAM, and that was pretty good for Rust development.
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
I've made some production services in Rust. I use a 16GB M1 and the thing compiles in under a minute. Incremental compilations are almost instant.
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.
I posted the parent comment. The required investment in new hardware for reasonable Rust developer experience cannot be understated. I took the poor hardware approach to software design with Rust and it came at the benefit of shorter compile times. Then, I worked with Rust monoliths that went all-in on macro-heavy libraries and felt the pain on my older workstation. It's interesting to think about the different decisions that one makes with the resources available, and constraints.
No one has corrected this yet, so I'll do it: it's 64GB of RAM!
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.
> A recent change[0] on nightly rustc might help with incremental builds.
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.
Rust by default only does a full build really rarely. Ive gone through days, even weeks, of working on a project without a full build.
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've got a ~9k loc Bevy "game" I'm writing that doesn't use dylib and takes > 20 secs every update, purely because of the linking (I'm not using mold yet, but am using lld)...
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)
Is there any new reason to believe that? What I had seen--and which I would have thought would be obvious--is that the C++ module design only helps at lower levels of parallelism and actually slows down the build dramatically at the higher levels of parallelism used by people who actually care about build performance; this is apparently because the inherent design of modules causes the same kind of pervasive coupling seen with Rust, destroying the inherent scalability of the classic C++ separate compilation model.
> 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).
I totally believe that would be true for the standard library--code which itself has no dependencies, is always unchanged by the user, and could frankly come first in essentially any build--but... is that also true versus a precompiled header, which always seemed to work fine for the standard library? The entire concept behind modules for your code seemed like it would limit, not improve, scalability. Put differently: what changed?
This could still be a dramatic improvement for numerical work that requires a lot of exploratory compilations and iterative style (i.e. steal back some market share from python)
Rust's only selling point is the way it tries to solve memory errors, but it's based on a style of memory management that is inferior. There are other styles that prevent such errors from occurring in the first place.
A lot of times I felt like deleting the cargo.lock file should have been done inbetween optimising the rust build. There were changes to the used crates, and sometimes those changes don't propogate before a `cargo clean`, `cargo update` or something like that.
I would be more interesting to know how easier it is to write a naive rust compiler than a c++ naive compiler, aka actual syntax complexity comparison.
To put that in perspective, how easier it is to write a naive rust compiler than a naive C compiler.
I think it's hard to draw absolute conclusions because small things can make a huge difference to build times.
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.
> I think it's hard to draw absolute conclusions because small things can make a huge difference to build times.
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.
> 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.
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.)
I'm not a Rust expert but I expect the basic compiler is doing things which just make it slow. With C++ compilers this can be the case too .
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.
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.
This (porting C++ line-by-line using unsafe Rust) might actually be an advantage for Rust when comparing build times. The compiler will be spending more time spent on the borrow checker if the port was actually written in idiomatic Safe Rust, because all the additional type checking and abstractions needed.
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.
> Do you think this would impact Rust build times?
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 ?
>> 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.
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.
> 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.
People would also complain that even though you claim “my C++ code is longer than Rust but compiles just as fast”, you’re not factoring the extra things Rust’s versions do
I gave up only a little beyond where they revealed that they ported line-by-line. I think this is an interesting experiment for those considering a Rust rewrite, but has no utility for a greenfield project. The scales are stacked strongly in favour of C++ here, without a doubt. Software designed for C++ has essentially zero chance of becoming idiomatic Rust without a huge amount of work, and tooling tends to be designed for idiomatic use
> I gave up only a little beyond where they revealed that they ported line-by-line.
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.
To be useful as a comparison between implementation languages, it should be idiomatic. Otherwise it just tells you what it's like to transliterate one language into another, something which is usually only done for interop purposes - running on a platform which doesn't have a native compiler, or to enable native-level APIs when foreign function support is poor.
Code size after transliteration is a race the target language can seldom win; abstractions that also exist (or close enough) in the target language get translated 1:1, abstractions which don't get broken down 1:n, and abstractions which exist in the target language but don't have analogues in the source language generally don't get used at all. It's hard to end up with a shorter program following this approach. Only boilerplate with redundant repetition and ceremony - code which has little functional effect other than to satisfy declaration order rules, symbol visibility and so on - can get eliminated.
My wording may have been unclear. I have no doubt you had good technical justification for this port. I'm purely talking about the experimental results regarding builds and toolchains here. For people starting a new project rather than porting, I don't think this is a useful data point
Well, for example the libs/container/src/linked_vector.rs is very overcomplicated and fragile using unsafe code. Ultimately what you want is for references to remain stable on push/pop, which can be achieved simply with a Vec<Vec<T>>.
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.
For those of us less versed in Rust, from what I can gather the reason the provided Vec<Vec<T>> implementation works is that the Vec implementation guarantees[1] no reallocation happens when created with a sufficient capacity, and that the elements live on the heap. This means it's safe for the outer Vec to be reallocated.
That said, the pop implementation seems faulty in that it does not appear to deallocate empty chunks. Does Rust have any non-obvious magic here?
> That said, the pop implementation seems faulty in that it does not appear to deallocate empty chunks. Does Rust have any non-obvious magic here?
No, the only magic here is my utter lack of testing for throwaway HN code :) A corrected implementation would be:
pub fn pop(&mut self) -> Option<T> {
let last = self.chunks.last_mut()?;
let ret = last.pop()?;
if last.is_empty() {
self.chunks.pop();
}
Some(ret)
}
This is only correct if we maintain the invariant that we do not keep any empty chunks around, but the rest of the implementation does not violate this.
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.
Does this article account for the fact that a rustc compilation does more things than simple C++ default compilation, where you need to run address sanitizers etc. to do the same as rustc?
That’s a language design issue, by choosing your language you choose much you want the compiler to do. So it doesn’t make sense to force that choice for the sake of comparison.
Similarly, Swift code is slow to compile for because it has a weird type system.
Compilation doesn't necessarily need strict checks, it's really just translating source code into machine code. It happens to be the case that we mostly do both using the same tools because compilers need to have a good understanding of type systems to do their job well, but it's not a strict requirement. A great counter example is esbuild, which will compile TypeScript but not perform actual type checks for performance reasons. This is possible because TypeScript types have no representation at runtime.
and are really only a development tool.
That doesn't seem relevant. The article isn't asking "is C++ better than rust?" or trying to make qualitative judgments on overall "worth" - it's an exploration of one area, an apples to apples (as close as possible, of course) comparison of compilation speed given a set of optimizations and constraints. Rust may do more analysis, c++ may have other tooling, but that's not relevant - the speed is the speed.
Sanitizers should primarily impact runtime performance anyways, but that's moot.
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.)
What about the other way around? Trying to cripple rustc until its features are closer to clang's? For example, removing some of the sanitizers or borrow checker in rustc?
And quite the reason why I think this was a poor choice of rust design because not being able to opt-out from static analysis, e.g. borrow checker, makes me having to pay the price for it every time even when I don't want to. This as a result has unfortunate consequences on iteration times, which C++ has been infamously known of but in this respect it plays better imo then rust.
Rust borrow checking is quick, as shown by cargo check. The problem with Rust compiles is (1) proc macros, when they are used and (2) the sheer amount of monomorphized generic code that rustc outputs for LLVM to deal with.
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.)
> 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.
Almost always the cost of borrow checker is trivial compared to code gen (and sometimes instantiation of generics). You can see that easily by comparing time of `cargo check` and `cargo build`.
The article does not account for this. I wanted the fastest build-test cycle. It sounds unfair to artificially slow down C++ compilation just because Rust has undisablable checks.
EDIT: Oops, I realized I already replied two hours ago.
I don't really like these comparisons looking at these languages through the lens of commonality. The only thing the two have in common is that they're systems languages.
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.
> 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.
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.
> I don't really like these comparisons looking at these languages through the lens of commonality. The only thing the two have in common is that they're systems languages.
>
> [...] 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.
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.
> 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 (...)
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.
> C++ is a bloated mess ... any one who argued (sic) about which language is better ... has completely missed the point of computer science and programming.
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.