300 karma · joined January 27, 2011
As a project grows larger, link times become more noticeable, and mold starts to help.
Do you think this would impact Rust build times?
> Reimplementing vectors and linked lists.
Someone has to implement them.
Note that they're also implemented in the C++ code, so the comparison is fair.
> Also, makes use of a ton of macros.
Can you show an example where the Rust code uses "a ton of macros"? The only place I can think of is the tests, but I thought macros were what you're supposed to use in Rust for assertions.
If you're referring to proc macros, I thought those were very popular in Rust.
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 think I only used iterators in places where there's no built-in function on slices like C++'s strchr and strspn. (I think Rust's str has these, but not [u8].) For example:
C++: https://github.com/quick-lint/cpp-vs-rust/blob/f8d31341f5cac...
std::size_t length = std::strcspn(c, separators);
if (c[length] == '\0') {
return found_separator{.length = length,
.which_separator = static_cast<std::size_t>(-1)};
}
const char* separator = std::strchr(separators, c[length]);
Rust: https://github.com/quick-lint/cpp-vs-rust/blob/f8d31341f5cac... match s
.as_bytes()
.iter()
.position(|c: &u8| separators.contains(c))
{
None => FoundSeparator {
length: s.len(),
which_separator: INVALID_WHICH_SEPARATOR,
},
Some(length) => {
let found_separator: u8 = unsafe { *s.as_bytes().get_unchecked(length) };
match separators.iter().position(|c: &u8| *c == found_separator) {This is not my experience.
Lifetime and '&mut self' noise (and four-space indentation) did cause rustfmt to sometimes split function signatures across multiple lines, but overall, I think rustfmt did a good job.
C++: https://github.com/quick-lint/cpp-vs-rust/blob/f8d31341f5cac...
lexer::parsed_identifier lexer::parse_identifier(const char8* input,
identifier_kind kind) {
const char8* begin = input;
const char8* end = this->parse_identifier_fast_only(input);
if (*end == u8'\\' || (kind == identifier_kind::jsx && *end == u8'-') ||
!this->is_ascii_character(*end)) {
return this->parse_identifier_slow(end,
/*identifier_begin=*/begin, kind);
} else {
return parsed_identifier{
.after = end,
.normalized = make_string_view(begin, end),
.escape_sequences = {},
};
}
}
Rust: https://github.com/quick-lint/cpp-vs-rust/blob/f8d31341f5cac... fn parse_identifier(
&mut self,
input: *const u8,
kind: IdentifierKind,
) -> ParsedIdentifier<'alloc, 'code> {
let begin: *const u8 = input;
let end: *const u8 = self.parse_identifier_fast_only(input);
let end_c: u8 = unsafe { *end };
if end_c == b'\\'
|| (kind == IdentifierKind::JSX && end_c == b'-')
|| !is_ascii_code_unit(end_c)
{
self.parse_identifier_slow(end, /*identifier_begin=*/ begin, kind)
} else {
ParsedIdentifier {
after: end,
normalized: unsafe { slice_from_begin_end(begin, end) },
escape_sequences: None,
}
}
}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.)
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.
In my experience, ASAN has a huge negative impact on build times for C++ code (Clang and GCC).
EDIT: Oops, I realized I already replied two hours ago.
I think C++20 modules are the promised golden ticket. It's 2023 and I still can't use them on Linux, though.
Nitpick: The article compares Clang, not GCC. (GCC is mentioned only to show how much faster Clang is.)
> you get so much more static analysis by default in rust compilation than C++.
I care about the build-test cycle, so adding static analysis is against my goals.
I would minimize the analyses done by rustc if I could.
Did you try upgrading your hardware? It really helps.
Based on my profiling, it seems like the code gen backend (LLVM) is a big offender. A code generator optimized for build times would be nice. (Cranelift seems to be a failure in that regard.)
C++ has an explicit template instantiation feature which helps reduce backend time. I wonder if this can be done in Rust too. Maybe this is what -Zshare-generics=y is for?
[1] the full project, not the 17k SLOC subset discussed in the article
I thought the opposite: header files (and CMakeLists.txt, which was included in the subtotal SLOC) artificially increase C++ SLOC.
> For example idiomatic Rust code can do with less defensive programming due to language features.
Can you show an example in the Rust code where this might be possible? https://github.com/quick-lint/cpp-vs-rust/tree/master/rust
My thinking was the opposite. Because Rust is newer, it doesn't have the bloaty baggage that C++ and its compilers have. And Rust's module system is (in theory) far better for build times than C++'s #include-based module system.
But instead of speculating, I put the time in to figure out the answer.
When I start a new programming project, I need to choose a language. For this project, three years ago, I had three serious choices:
* C * C++ * Rust
I rejected C because I wanted the productivity boost from C++ or Rust. I rejected Rust because Rust was new to me (thus risky) but I was very familiar with C++.
I don't see why it's a bad idea to compare languages—especially along specific dimensions, like run-time perf or build time or safety—when that's what us software engineers do whenever we start a new project.
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.
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.)
There are. And democracy makes politicians seem more legitimate. Democracy in the wrong place can make corruption worse.
Hiring cops should not be a popularity contest (i.e. democratic) in my opinion.