From personal experience I was expecting the answer to be yes, but really hoping reading this blog that it was the opposite and there was a magical switch to flip to make it all better.
The only thing I’ve found to make a material difference is running sccache locally and in CI, and using it to cache build artifacts. I can’t imagine building without sccache as it makes a noticeable difference to rebuilds, but it’s not enough. On GitHub this is the action I use to cache: https://github.com/stellar/actions/blob/main/rust-cache/action.yml.
It’s not just build time, but also build resources. On GitHub Actions I’ve lost hours debugging not enough disk space errors, trying to keep a workspace build under the disk space limitations.
It’s probably worth noting that this is a problem only for large projects. Yes, a CI build of LLVM for us takes 20 minutes (16 core Azure VM), but LLVM is one of the largest C++ codebases that I work on and even then incremental builds are often a few seconds (linking debug builds takes longer because of the vast quantity of debug info).
There’s always going to be a trade off between build speed and performance: moving work from run time to compile time is always going to slow down compile speed. The big problem with C++ builds is the amount of duplicated work. Templates are instantiated at use point and that instantiations is not shared between compilation units. I wouldn’t be surprised if around 20% of compilation units in LLVM have duplicate copies of the small vector class instantiated on an LLVM value. Most of these are inlined and then all except one of the remainder is thrown away in the final link. The compilation database approach and modules can reduce this by caching the instantiations and sharing them across compilation units. This is something that a language with a more overt module system can definitely improve because dependency tracking is simpler. If a generic collection and a type over which it’s instantiated are in separate files then it’s trivial to tell (with a useful over approximation) when the instantiations needs to be regenerated: when either of the files has changed. With C++, you need to check that no other tokens in the source file have altered the shape of the AST. C++ modules help a lot here, by providing includable units that generate a consistent AST independent of their use order. Rust has had a similar abstraction from the start (as have most non-C languages).
I think that’s true for C++ but I use Rust for small projects and I definitely find compile times to still be a problem. Rust has completely replaced my usage of C++ but is strictly worse in terms of compilation time for my projects. Even incremental builds can be really slow because if you depend on a lot of crates (which is incredibly easy to do transitively), linking time will dominate. mold helps a lot here.
I think this is always going to be a problem regardless of the compiler or optimizations because it’s just too tempting to use advanced macros / type level logic or pull in too many dependencies. In C++ I would just avoid templates and other slow to compile patterns, but in Rust these utilities are too powerful and convenient to ignore. Most of the Rust versions of a given library will try to do something cool with types and macros that’s usually great but you end up paying for it in compile times. I hope the Rust community starts to see compile times as a more pressing concern and designing crates with that in mind.
I use Rust for small projects and I definitely find compile times to still be a problem.
This is my experience. Unless we’re talking about ultra tiny workspaces, any small to medium sized project hurts, especially compared to build times with other tool chains like the Go compiler.
FYI: for my own domain (backend web services), if I were going to switch to a statically-typed + compiled language, Go would still be very far down the list – the top choice would almost certainly be C#. The tooling and ecosystem are mature and fantastically rich, the performance is great (both compile-time and run-time), and the language has learned a lot both from predecessors and from itself.
Using dependencies is much easier and faster with Go, though.
I’ve tried using SDL2 in 20 different programming languages. I really liked the C# version, but getting the SDL2 dependency to work was much harder than from Go. I ended up calling SDL2 directly from C# instead of using an intermediate library.
A very interesting article with very interesting conclusions, thanks for sharing. The author has put a considerable amount of effort into his experiments. The results appear credible. I’m not sure though Rust is really a replacement for C++; the philosophy of these two languages is rather different.
If by philosophy you mean C++ leaves the safety up to you and your fellow programmers and Rust gives you tools to enforce the safety then yes. But Rust targets the same use cases as C++. It has RAII, Zero Cost Abstractions, and a number of other elements for working closer to the metal. On top of that it adds a much better type system, temporal memory safety, and a package manager. Rust is exactly what you should want from a replacement for C++. The compile times will eventually catch up I think.
This is too exaggerated; on the one hand Rust only takes care of a part of the safety aspects, and on the other hand you can avoid most memory-issues very well in C++ with bord-means. Rust has an advantage primarily in that the memory management can be delegated to the compiler, which in principle brings a better runtime performance with it (not surprisingly compiling becomes more expensive).
But Rust targets the same use cases as C++
C++ has full OOP support; at latest if you want to migrate a project relying on this a full redesign is required.
Rust is exactly what you should want from a replacement for C++
Unfortunately not yet. I would consider it as a possible C or Go replacement.
C++ has full OOP support; at latest if you want to migrate a project relying on this a full redesign is required.
OOP is a design choice, not an inherent constraint.
When people say Rust is a C++ replacement, I believe they mean that Rust can be used wherever C++ is used for, not necessarily that it’s easy to migrate an existing project from C++ to Rust. There are relatively few languages that target the latter niche, and I think they are better called “C++ successor language” (borrowed from the title of a recent article).
If you accept my definition, I think there are really just two things that matter for a language to be considered a “C++ replacement”:
“As performant as C++”, in other words zero-cost abstraction
This is an explicit goal of Rust, and it’s relatively easy to say whether an abstraction is zero-cost. This criteria will immediately exclude the majority of languages designed today, which almost always contain some non-zero-cost abstraction, most typically GC.
“As powerful as C++”, in other words some reasonable set of high-level abstraction mechanisms
This is very open to debate, but it is possible to make a holistic judgement without dwelling too much in specific language features. If you look at the system of abstractions Rust provide as a whole, most people would agree that it has a similar (or higher) level of power compared to C++.
OOP is a design choice, not an inherent constraint.
Well, it’s true that you can do OOP also in C or even in assembler, but it’s definitely very unpractical. So it’s quite relevant if a language has built-in OO or not if your project requires an OO architecture.
I believe they mean that Rust can be used wherever C++ is used
Many of these claims are made by people not even mentioning C++ on their CV. Unfortunately, software engineering is not free of ideologies; otherwise, one would simply optimize soberly for the best way to solve a problem, be it C++, Rust or whatever, without any romantic or mystical feelings towards the technology.
What project “requires” an OO structure? That seems like it’s forcing a solution onto a problem and then complaining the language doesn’t support that. Turing Completeness yadda yadda
You can avoid memory issues with C++. But the type system won’t tell you if you made a mistake. Rust’s type system will. This means that you can rely not only on your own code, but also on other peoples code. We have multiple decades of software development that tells us when it comes to C++ not only can you not trust other peoples code, you can’t even trust your own code. You will and in fact probably already have made mistakes that result in violations of temporal memory safety. Even in projects that heavily use Static Analyzers, Linters, and tools like Valgrind the evidence is that C++ can’t produce memory safe code reliably. Starting a new project in C++ is starting to look very unwise.
Rust has all the OOP support that is useful and leaves behind the stuff that has massive footguns. I’m looking at you Multiple Inheritance.
No language including Rust should be normally be looked at for rewriting pre-existing C++ projects. That is almost alway a waste of time. You can absolutely use it to slowly replace portions of an existing project though. (See Mozilla Firefox).
From a purely pragmatic and evidence based standpoint the only reason to think that Rust isn’t a better replacement for the things you would use C++ for is because you just don’t like it. That’s personal and subjective and no one can argue against it. But it does require acknowledging that you are introducing a large risk factor for memory vulnerabilities into your code when you continue using it. If you disagree then the evidence is very much against you. If you are fine with that risk then cool but don’t pretend the risk isn’t there.
Rust has all the OOP support that is useful and leaves behind the stuff that has massive footguns. I’m looking at you Multiple Inheritance.
Inheritance is a core part of OOP. Last I heard, Rust only has inheritance of interfaces (traits), not implementations. Same limitation as Go. This is really only half of OOP and makes it very awkward to use a fully OO design. (I’ve tried to do it in Go and gave up in frustration.)
Inheritance is widely regarded as a mistake in our industry. Anything you can accomplish with with implementation inheritance you can accomplish with composition. Traits help to make composition more ergonomic. If you consider inheritance a core part of a fully OO design then yes. Rust won’t help. If however you consider sharing implementations the real core part of an OO design which most of our industry agrees is the real point then you have everything you need from OO principles.
I stand by my statement. I’ve never had a problem doing fully OO design in Go or Rust. And in Java which has inheritance all of my OO designs avoid inheritance due to the problems it introduces to the codebase. Instead they use composition and interfaces. Everyone I know who uses Java also avoids it. The issues of implementation inheritance as a way of having code reuse are well documented online.
If you try to program Haskell like it’s Java you’ll also have problems but that’s not an issue with Haskell. Different languages promote different types of abstraction and forcing your particular abstraction onto a language is basically doomed to failure. Also it’s worth noting that as time has gone on, inheritance has increasingly fallen out of favor, with most OO design guides reccomending composition as the go to tool and inheritance being used sparingly.
One thing I’m doing on Oil is to keep track of the total size of expanded input passed to the compiler.
Basically I have Ninja targets for each file that do gcc -E, and then another target that passes them to wc.
It is pretty eye opening – it fluctuates a lot as the code changes, and keeping an eye on it has resulted in real build time improvements. It doesn’t perfectly correlate with build times, but it’s a lot easier to measure and optimize – by removing unused #includes and restructuring translation units.
In the next release, the total will be down to ~795K lines (from 1.1M you see) because I removed <unordered_set> from the GC implementation. As mentioned, the total size fluctuates a lot!
Projects like Google Chromium take an hour to build on brand new hardware and 6 hours to build on older hardware.
A clean build, yes. How often do you need to do a clean build? It’s been many years since I worked on Chromium, but back then an incremental debug build, the kind I did all day long, took about 45 seconds on my medium-spec iMac. (The large majority of that was in the linker.) Not great, but not awful.
I don’t really keep up with Rust, but last I heard, incremental builds were sort of a bleeding-edge feature. Are they solid now? Can you get sub-minute turnaround in a huge project?
From personal experience I was expecting the answer to be yes, but really hoping reading this blog that it was the opposite and there was a magical switch to flip to make it all better.
The only thing I’ve found to make a material difference is running sccache locally and in CI, and using it to cache build artifacts. I can’t imagine building without
sccache
as it makes a noticeable difference to rebuilds, but it’s not enough. On GitHub this is the action I use to cache: https://github.com/stellar/actions/blob/main/rust-cache/action.yml.It’s not just build time, but also build resources. On GitHub Actions I’ve lost hours debugging not enough disk space errors, trying to keep a workspace build under the disk space limitations.
I frequently run out of disk space, just building some TUI programs locally, and ending up with many gigabytes of output.
I now have a script that removes
<any-dir-with-Cargo.toml>/target
.It’s probably worth noting that this is a problem only for large projects. Yes, a CI build of LLVM for us takes 20 minutes (16 core Azure VM), but LLVM is one of the largest C++ codebases that I work on and even then incremental builds are often a few seconds (linking debug builds takes longer because of the vast quantity of debug info).
There’s always going to be a trade off between build speed and performance: moving work from run time to compile time is always going to slow down compile speed. The big problem with C++ builds is the amount of duplicated work. Templates are instantiated at use point and that instantiations is not shared between compilation units. I wouldn’t be surprised if around 20% of compilation units in LLVM have duplicate copies of the small vector class instantiated on an LLVM value. Most of these are inlined and then all except one of the remainder is thrown away in the final link. The compilation database approach and modules can reduce this by caching the instantiations and sharing them across compilation units. This is something that a language with a more overt module system can definitely improve because dependency tracking is simpler. If a generic collection and a type over which it’s instantiated are in separate files then it’s trivial to tell (with a useful over approximation) when the instantiations needs to be regenerated: when either of the files has changed. With C++, you need to check that no other tokens in the source file have altered the shape of the AST. C++ modules help a lot here, by providing includable units that generate a consistent AST independent of their use order. Rust has had a similar abstraction from the start (as have most non-C languages).
I think that’s true for C++ but I use Rust for small projects and I definitely find compile times to still be a problem. Rust has completely replaced my usage of C++ but is strictly worse in terms of compilation time for my projects. Even incremental builds can be really slow because if you depend on a lot of crates (which is incredibly easy to do transitively), linking time will dominate. mold helps a lot here.
I think this is always going to be a problem regardless of the compiler or optimizations because it’s just too tempting to use advanced macros / type level logic or pull in too many dependencies. In C++ I would just avoid templates and other slow to compile patterns, but in Rust these utilities are too powerful and convenient to ignore. Most of the Rust versions of a given library will try to do something cool with types and macros that’s usually great but you end up paying for it in compile times. I hope the Rust community starts to see compile times as a more pressing concern and designing crates with that in mind.
This is my experience. Unless we’re talking about ultra tiny workspaces, any small to medium sized project hurts, especially compared to build times with other tool chains like the Go compiler.
Fantastic overview and comparison of methods to tweak compile time. I hoped to see a magical solution as well.
I must say that Rust’s safety makes it up to me.
I know it’s not a favorite among rustaceans, but If you care about compile times and short development cycles, this is where Go shines.
I would go as far as claiming that short complle times for larger projects is the whole point of using Go.
FYI: for my own domain (backend web services), if I were going to switch to a statically-typed + compiled language, Go would still be very far down the list – the top choice would almost certainly be C#. The tooling and ecosystem are mature and fantastically rich, the performance is great (both compile-time and run-time), and the language has learned a lot both from predecessors and from itself.
Using dependencies is much easier and faster with Go, though.
I’ve tried using SDL2 in 20 different programming languages. I really liked the C# version, but getting the SDL2 dependency to work was much harder than from Go. I ended up calling SDL2 directly from C# instead of using an intermediate library.
https://github.com/xyproto/sdl2-examples
Also, as far as I know, C# does not have the equivalent to
defer
in Go.A very interesting article with very interesting conclusions, thanks for sharing. The author has put a considerable amount of effort into his experiments. The results appear credible. I’m not sure though Rust is really a replacement for C++; the philosophy of these two languages is rather different.
If by philosophy you mean C++ leaves the safety up to you and your fellow programmers and Rust gives you tools to enforce the safety then yes. But Rust targets the same use cases as C++. It has RAII, Zero Cost Abstractions, and a number of other elements for working closer to the metal. On top of that it adds a much better type system, temporal memory safety, and a package manager. Rust is exactly what you should want from a replacement for C++. The compile times will eventually catch up I think.
This is too exaggerated; on the one hand Rust only takes care of a part of the safety aspects, and on the other hand you can avoid most memory-issues very well in C++ with bord-means. Rust has an advantage primarily in that the memory management can be delegated to the compiler, which in principle brings a better runtime performance with it (not surprisingly compiling becomes more expensive).
C++ has full OOP support; at latest if you want to migrate a project relying on this a full redesign is required.
Unfortunately not yet. I would consider it as a possible C or Go replacement.
OOP is a design choice, not an inherent constraint.
When people say Rust is a C++ replacement, I believe they mean that Rust can be used wherever C++ is used for, not necessarily that it’s easy to migrate an existing project from C++ to Rust. There are relatively few languages that target the latter niche, and I think they are better called “C++ successor language” (borrowed from the title of a recent article).
If you accept my definition, I think there are really just two things that matter for a language to be considered a “C++ replacement”:
“As performant as C++”, in other words zero-cost abstraction
This is an explicit goal of Rust, and it’s relatively easy to say whether an abstraction is zero-cost. This criteria will immediately exclude the majority of languages designed today, which almost always contain some non-zero-cost abstraction, most typically GC.
“As powerful as C++”, in other words some reasonable set of high-level abstraction mechanisms
This is very open to debate, but it is possible to make a holistic judgement without dwelling too much in specific language features. If you look at the system of abstractions Rust provide as a whole, most people would agree that it has a similar (or higher) level of power compared to C++.
Well, it’s true that you can do OOP also in C or even in assembler, but it’s definitely very unpractical. So it’s quite relevant if a language has built-in OO or not if your project requires an OO architecture.
Many of these claims are made by people not even mentioning C++ on their CV. Unfortunately, software engineering is not free of ideologies; otherwise, one would simply optimize soberly for the best way to solve a problem, be it C++, Rust or whatever, without any romantic or mystical feelings towards the technology.
What project “requires” an OO structure? That seems like it’s forcing a solution onto a problem and then complaining the language doesn’t support that. Turing Completeness yadda yadda
You can avoid memory issues with C++. But the type system won’t tell you if you made a mistake. Rust’s type system will. This means that you can rely not only on your own code, but also on other peoples code. We have multiple decades of software development that tells us when it comes to C++ not only can you not trust other peoples code, you can’t even trust your own code. You will and in fact probably already have made mistakes that result in violations of temporal memory safety. Even in projects that heavily use Static Analyzers, Linters, and tools like Valgrind the evidence is that C++ can’t produce memory safe code reliably. Starting a new project in C++ is starting to look very unwise.
Rust has all the OOP support that is useful and leaves behind the stuff that has massive footguns. I’m looking at you Multiple Inheritance.
No language including Rust should be normally be looked at for rewriting pre-existing C++ projects. That is almost alway a waste of time. You can absolutely use it to slowly replace portions of an existing project though. (See Mozilla Firefox).
From a purely pragmatic and evidence based standpoint the only reason to think that Rust isn’t a better replacement for the things you would use C++ for is because you just don’t like it. That’s personal and subjective and no one can argue against it. But it does require acknowledging that you are introducing a large risk factor for memory vulnerabilities into your code when you continue using it. If you disagree then the evidence is very much against you. If you are fine with that risk then cool but don’t pretend the risk isn’t there.
Inheritance is a core part of OOP. Last I heard, Rust only has inheritance of interfaces (traits), not implementations. Same limitation as Go. This is really only half of OOP and makes it very awkward to use a fully OO design. (I’ve tried to do it in Go and gave up in frustration.)
Inheritance is widely regarded as a mistake in our industry. Anything you can accomplish with with implementation inheritance you can accomplish with composition. Traits help to make composition more ergonomic. If you consider inheritance a core part of a fully OO design then yes. Rust won’t help. If however you consider sharing implementations the real core part of an OO design which most of our industry agrees is the real point then you have everything you need from OO principles.
I stand by my statement. I’ve never had a problem doing fully OO design in Go or Rust. And in Java which has inheritance all of my OO designs avoid inheritance due to the problems it introduces to the codebase. Instead they use composition and interfaces. Everyone I know who uses Java also avoids it. The issues of implementation inheritance as a way of having code reuse are well documented online.
If you try to program Haskell like it’s Java you’ll also have problems but that’s not an issue with Haskell. Different languages promote different types of abstraction and forcing your particular abstraction onto a language is basically doomed to failure. Also it’s worth noting that as time has gone on, inheritance has increasingly fallen out of favor, with most OO design guides reccomending composition as the go to tool and inheritance being used sparingly.
Curiously, there is a way to emulate structure methods and even fields inheritance in Rust: https://rustwasm.github.io/wasm-bindgen/web-sys/inheritance.html.
[Comment removed by author]
The biggest drag on C++ compile times is #include statements. Header files also make C++ more verbose. As the author notes.
But C++20 has modules, which are supposed to fix these problems. Someone should compare C++20 to Rust. The benchmarks should have better results.
One thing I’m doing on Oil is to keep track of the total size of expanded input passed to the compiler.
Basically I have Ninja targets for each file that do
gcc -E
, and then another target that passes them towc
.It is pretty eye opening – it fluctuates a lot as the code changes, and keeping an eye on it has resulted in real build time improvements. It doesn’t perfectly correlate with build times, but it’s a lot easier to measure and optimize – by removing unused #includes and restructuring translation units.
https://www.oilshell.org/release/0.13.1/pub/metrics.wwz/preprocessed/cxx-opt.txt
This relies on our new “declarative Ninja / mini-bazel” build system which I am growing very fond of
http://www.oilshell.org/blog/2022/10/garbage-collector.html#declarative-ninja-mini-bazel
In the next release, the total will be down to ~795K lines (from 1.1M you see) because I removed
<unordered_set>
from the GC implementation. As mentioned, the total size fluctuates a lot!Unfortunately, modules still don’t work in most compilers.
import <iostream>;
gives an error.That the Rust code is longer is more surprising to me than the build times. I’m curious how idiomatic it is.
A clean build, yes. How often do you need to do a clean build? It’s been many years since I worked on Chromium, but back then an incremental debug build, the kind I did all day long, took about 45 seconds on my medium-spec iMac. (The large majority of that was in the linker.) Not great, but not awful.
I don’t really keep up with Rust, but last I heard, incremental builds were sort of a bleeding-edge feature. Are they solid now? Can you get sub-minute turnaround in a huge project?
I do a clean build of LLVM frequently. Our CI server does it about 48x per day.