8 ms·
In short, the maximum possible speed is the same (+/- some nitpicks), but there can be significant differences in typical code, and it's hard to define what's a
by pornel 9mo ago
In short, the maximum possible speed is the same (+/- some nitpicks), but there can be significant differences in typical code, and it's hard to define what's a realistic typical example.
The big one is multi-threading. In Rust, whether you use threads or not, all globals must be thread-safe, and the borrow checker requires memory access to be shared XOR mutable. When writing single-threaded code takes 90% of effort of writing multi-threaded one, Rust programmers may as well sprinkle threads all over the place regardless whether that's a 16x improvement or 1.5x improvement. In C, the cost/benefit analysis is different. Even just spawning a thread is going to make somebody complain that they can't build the code on their platform due to C11/pthread/openmp. Risk of having to debug heisenbugs means that code typically won't be made multi-threaded unless really necessary, and even then preferably kept to simple cases or very coarse-grained splits.
- OptionOfT 9mo agoApart from multi threading, there is more information in the Rust type system. Would that would allow more optimizations?
- adgjlsfhk1 9mo agoYes. Specifically since Rust's design prevents shared mutablity, if you have 2 mutable data-structures you know that they don't alias which makes auto vectorization a whole lot easier.
- kouteiheika 9mo agoYes. All `&mut` references in Rust are equivalent to C's `restrict` qualified pointers. In the past I measured a ~15% real world performance improvement in one of my projects due to this (rustc has/had a flag where you can turn this on/off; it was disabled by default for quite some time due to codegen bugs in LLVM).
- steveklabnik 9mo agoNot just all &mut T, but also all &T, where the T does not transitively contain an UnsafeCell<T>. Click "show llvm ir" instead of "build" here: https://play.rust-lang.org/?version=stable&mode=release&edition=2024&gist=34cca2d4f7dbaf1d4ef5d9ef1c3e7d3b https://play.rust-lang.org/?version=stable&mode=release&edit...
- marcianx 9mo agoI was confused by this at first since `&T` clearly allows aliasing (which is what C's `restrict` is about). But I realize that Steve meant just the optimization opportunity: you can be guaranteed that (in the absence of UB), the data behind the `&T` can be known to not change in the absence of a contained `UnsafeCell<T>`, so you don't have to reload it after mutations through other pointers.
- steveklabnik 9mo agoYes. It's a bit tricky to think about, because while it is literally called 'noalias', what it actually means is more subtle. I already linked to a version of the C spec below, https://www.open-std.org/jtc1/sc22/wg14/www/docs/n3220.pdf https://www.open-std.org/jtc1/sc22/wg14/www/docs/n3220.pdf but if anyone is curious, this part is in "6.7.4.2 Formal definition of restrict" on page 122. In some ways, this is kind of the core observation of Rust: "shared xor mutable". Aliasing is only an issue if the aliasing leads to mutability. You can frame it in terms of aliasing if you have to assume all aliases can mutate, but if they can't, then that changes things.
- salmon640 9mo ago[flagged]
- dmitrygr 9mo agoDo you not use restrict in your normal everyday C code that you write? I use it in my normal C code.
- kouteiheika 9mo agoI used to use it, but very rarely, since it's instant UB if you get it wrong. In tiny codebases which you can hold in your head it's probably practical to sprinkle it everywhere, but in anything bigger it's quite risky. Nevertheless, I don't write normal everyday C code anymore since Rust has pretty much made it completely obsolete for the type of software I write.
- kazinator 9mo agorestrict works by making some situations undefined behavior that would otherwise be defined without it. It is probably unwise to use casually or habitually.
- kazinator 9mo agoBut of course the only thing restrict does in C is potentially introduce certain kinds of undefined behavior into a program that would be correct without it (and then things can be optimized on the assumption that the code is not invoked in a way that it would happen)
- mhh__ 9mo agoAliasing info is gold dust to a compiler in various situations although the absence of it in the past can mean that they start smoking crack when it's provided.
- tcfhgj 9mo agowhat about generics (equivalent to templates in C++), which allow compile time optimizations all the way down which may not possible if the implementation is hidden behind a void*?
- OptionOfT 9mo agoUnless you use `dyn`, all code is monomorphized, and that code on its own will get optimized. This does come with code-bloat. So the Rust std sometimes exposes a generic function (which gets monomorphized), but internally passes it off to a non-generic function. This to avoid that the underlying code gets monomorphized. https://github.com/rust-lang/rust/blob/8c52f735abd1af9a73941b78fe7ed2ab08b9c0dd/library/std/src/path.rs#L2695-L2707 https://github.com/rust-lang/rust/blob/8c52f735abd1af9a73941...
- dwattttt 9mo ago> This does come with code-bloat. So the Rust std sometimes exposes a generic function (which gets monomorphized), but internally passes it off to a non-generic function. There's no free lunch here. Reducing the amount of code that's monomorphised reduces the code emitted & improves compile times, but it reduces the scope of the code that's exposed to the input type, which reduces optimisation opportunities.
- josephg 9mo agoYes. But I like that rust gives you the option. In C, the only way to write a monomorphized hash table or array list involves horribly ugly macros that are difficult to write and debug. Rust does monomorphization by default, but you can also use &dyn trait for vtable-like behaviour if you prefer.
- bonzini 9mo agoThere is also the "momo" crate to do the same with a procedural macro attribute (https://docs.rs/momo/latest/momo/ https://docs.rs/momo/latest/momo/).
- randomNumber7 9mo agoIn C there is the "restrict" keyword to tell the compiler that there is no other pointer to the values accessed over a certain pointer. If you do not use that the generated code can be quite suboptimal in certain cases.
- throwaway2037 9mo ago> If you do not use that the generated code can be quite suboptimal in certain cases. I believe you, but I don't understand it. Can you give a simple example to demonstrate your point?
- bennettnate5 9mo agoThe simplest example is `memcpy(dst, src, len)` and similar iterative byte copying operations. If the function did not use noalias, the compiler wouldn't be free to optimize individual byte read/writes into register-sized writes, as the destination may overlap with the source. In practice this means 8x more CPU instructions per copy operation on a 64-bit machine. Note that memcpy specifically may already be implemented this way under the hood because it requires noalias; but I imagine similar iterative copying operations can be optimized in a like manner ad-hoc when aliasing information is baked in like it is with Rust.
- repstosb 9mo agoThat's not a great example, since memcpy() already has all the information it needs to determine whether the regions overlap (src < dest + len && dst < src + len) and even where and by how much. So pretty much any quality implementation is already performing this test and selecting an appropriate code path, at the cost of a single branch rather than 8x as many memory operations. The real purpose of restrict is to allow the compiler to cache a value that may be used many times in a loop (in memcpy(), each byte/word is used only once) in a register at the start of the loop, and not have to worry about repeatedly reaching back to memory to re-retrieve it because it might have been modified as a side effect of the loop body.
- 9mo ago
- m-schuetz 9mo agoI'm still confused as to why linux requires linking against TBB for multithreading, thus breaking cmake configs without if(linux) for tbb. That stuff should be included by default without any effort by the developer.
- sebtron 9mo agoI think this is related to the C++ standard library implementation. Using pthread in C, for example, TBB is not required. Not sure about C11 threads, but I have always thought that GLIBC just uses pthread under the hood.
- m-schuetz 9mo agoI don't know the details since I'm mainly a windows dev, but when porting to linux, TBB has always been a huge pain in the ass since it's a suddenly additionally required dependency by gcc. Using C++ and std::thread.
- pjmlp 9mo agoAlso clang, and in general parallel algorithms aren't available outside of platforms not supported by TBB. C++26 will get another similar dependency, because BLAS algorithms are going to be added, but apparently the expectation is to build on top of C/Fortran BLAS battle tested implementations.
- groundzeros2015 9mo agoMultithreading does not make code more efficient. It still takes the same amount of work and power (slightly more). On a backend system where you already have multiple processes using various cores (databases, web servers, etc) it usually doesn’t make sense as a performance tool. And on an embedded device you want to save power so it also rarely makes sense.
- NetMageSCW 9mo agoMultithreading can made an application more responsive and more performant to the end user. If multithreading causes an end user to have to wait less, the code is more performant.
- groundzeros2015 9mo agoYes it can used to reduce latency of a particular task. Did you read my points about when it’s not helpful? Are people making user facing apps in rust with GUIs?
- tcfhgj 9mo ago> Are people making user facing apps in rust with GUIs? yes
- groundzeros2015 9mo agogot any to share? Should I assume native gui in these these rust performance debates?
- tcfhgj 9mo agohttps://system76.com/cosmic https://system76.com/cosmic https://helix-editor.com/ https://helix-editor.com/ https://zed.dev/ https://zed.dev/
- 9mo ago
- arghwhat 9mo agoTo be honest, I think a lot of the justification here is just a difference in standard library and ease of use. I wouldn't consider there to be any notable effort in making thread build on target platforms in C relative to normal effort levels in C, but it's objectively more work than `std::thread::spawn(move || { ... });`. Despite benefits, I don't actually think the memory safety really plays a role in the usage rate of parallelism. Case in point, Go has no implicit memory safety with both races and atomicity issues being easy to make, and yet relies much heavier on concurrency (with a parallelism degree managed by the runtime) with much less consideration than Rust. After all, `go f()` is even easier. (As a personal anecdote, I've probably run into more concurrency-related heisenbugs in Go than I ever did in C, with C heisenbugs more commonly being memory mismanagement in single-threaded code with complex object lifetimes/ownership structures...)
- nineteen999 9mo ago> (As a personal anecdote, I've probably run into more concurrency-related heisenbugs in Go than I ever did in C, with C heisenbugs more commonly being memory mismanagement in single-threaded code with complex object lifetimes/ownership structures...) This is my experience too.
- kannujd 9mo ago[flagged]
- pornel 9mo agoGo is weirdly careless about thread-safety of its built-in data structures, but GC, channels, and the race detector seem to be enough?
- Mawr 9mo agoGo generally takes the "eh, good enough" approach ;)
- majormajor 9mo ago> (As a personal anecdote, I've probably run into more concurrency-related heisenbugs in Go than I ever did in C, with C heisenbugs more commonly being memory mismanagement in single-threaded code with complex object lifetimes/ownership structures...) Is that beyond just "concurrency is tricky and a language that makes it easier to add concurrency will make it easier to add sneaky bugs"? I've definitely run into that, but have never written concurrent C to compare the ease of heisenbug-writing.
- gpderetta 9mo agoThen again, often #pragma omp for is a very low mental-overhead way to speed up code.
- MeetingsBrowser 9mo agoDepends on the code. OpenMP does nothing to prevent data races, and anything beyond simple for loops quickly becomes difficult to reason about.
- thesz 9mo agoNo. It is easy to divide loop body into computation and share info update, the latter can be done under #pragma omp critical (label).
- nurettin 9mo agoYes! gcc/omp in general solved a lot of the problems which are conveniently left out in the article. The we have the anecdotal "They failed firefox layout in C++ twice then did it in Rust" < to this I sigh in chrome.
- steveklabnik 9mo agoThe Rust version of this is "turn .iter() into .par_iter()." It's also true that for both, it's not always as easy as "just make the for loop parallel." Stylo is significantly more complex than that. > to this I sigh in chrome. I'm actually a Chrome user. Does Chrome do what Stylo does? I didn't think it did, but I also haven't really paid attention to the internals of any browsers in the last few years.
- pjmlp 9mo agoAnd the C++ version is add std::execution::par_unseq as parameter to the ranges algorithm.
- MeetingsBrowser 9mo ago
- jasonjmcghee 9mo ago> Rust programmers may as well sprinkle threads all over the place regardless whether that's a 16x improvement or 1.5x improvement What about energy use and contention?
- pornel 9mo agoUsually it's a benefit for energy usage anyway. CPUs are most energy efficient sitting idle doing nothing, so finishing work sooner in wall-clock time usually helps despite overheads. Energy usage is most affected by high clock frequencies, and CPUs will boost clocks for single-threaded code. Threads waiting on cache misses let CPU use hyperthreading, which is actually energy efficient (you get context switching in hardware). You can waste energy in pathological cases if you overuse spinlocks or spawn so many threads that bookkeeping takes more work than what the threads do, but helper libraries for multithreading all have thread pools, queues, and dynamic work splitting to avoid extreme cases. Most of the time low speed up is merely Amdahl's law – even if you can distribute work across threads, there's not enough work to do.
- jasonjmcghee 9mo agoThanks
- stonogo 9mo agoEven just spawning a thread is going to make somebody complain that they can't build the code on their platform due to C11/pthread/openmp. This matches squarely with my experience, but it's not limited to threading, and Rust evades a large swath of these problems by relatively limited platform support. I look forward to the day I can run Rust wherever I run C!
- rtpg 9mo agoWhile Rust doesn't have C coverage, it has (by my last check) better coverage than something like CPython currently does. The big thing though is Rust is honest about their tiers of support, whereas for many projects "supported platform" for minor platforms often mean "it still compiles (at least we think it does, when the maintainer tries it and it fails they will fix it)" Not to be too glib though, there are obviously tools out there that have as much or more rigor than Rust and cover more platforms. Just... "supported platforms" means different things in different contexts.
- stonogo 9mo agoAll too common (not just with compilers) for someone to port the subset they care about and declare it done. Rust's decision to create standards of compliance and be conscious about which platforms are viable targets and which ones don't meet their needs is a completely valid way to ensure that whole classes of trouble never come. I think it's a completely valid approach, despite complaints from some.
- dochtman 9mo agoI think the way Rust checks borrows also makes it a lot more feasible to avoid allocations/copies; not because it is impossible to do in C, but because doing it in C requires writing very careful documentation and the caller to actually read that documentation. In (safe) Rust this is all checked by the compiler such that libraries can leverage it without blowing their complexity budget.
- throwaway2037 9mo ago> When writing single-threaded code takes 90% of effort of writing multi-threaded one That "when" is doing some heavy lifting! More seriously: You raise a very interesting point. When I moved from C++ to Java (10+ years ago), I was initially so nervous to add threads to my Java code. Why? Because it was (then) difficult and dangerous to do it in C++. C++ debuggers were awful, so I didn't think I could debug problems with multi-threaded C++ code. (Of course, the C++ ecosystem has drastically improved in the last 10 years, so I am sure it is now much more pleasant (and safe) to write multi-threaded C++ code.) When I finally sat down to add threads to some Java code, I could not believe how easy it was, including debugging. As a result, going forward, I was much more likely to add threads to my Java... or even start with a multi-threaded design, even if there is only a modest performance improvement.
- yobbo 9mo agoIn C, one can build data structures with pointers that would require reference counting and heap allocation in Rust. The performance would also depend on what kind of CPU/features it is compiled for. Whether one should do it is a different question.
- steveklabnik 9mo agoYou can use unsafe in Rust to write the exact same thing.