| ▲ | OskarS 16 hours ago |
| I think personally the answer is "basically no", Rust, C and C++ are all the same kind of low-level languages with the same kind of compiler backends and optimizations, any performance thing you could do in one you can basically do in the other two. However, in the spirit of the question: someone mentioned the stricter aliasing rules, that one does come to mind on Rust's side over C/C++. On the other hand, signed integer overflow being UB would count for C/C++ (in general: all the UB in C/C++ not present in Rust is there for performance reasons). Another thing I thought of in Rust and C++s favor is generics. For instance, in C, qsort() takes a function pointer for the comparison function, in Rust and C++, the standard library sorting functions are templated on the comparison function. This means it's much easier for the compiler to specialize the sorting function, inline the comparisons and optimize around it. I don't know if C compilers specialize qsort() based on comparison function this way. They might, but it's certainly a lot more to ask of the compiler, and I would argue there are probably many cases like this where C++ and Rust can outperform C because of their much more powerful facilities for specialization. |
|
| ▲ | ndesaulniers 6 hours ago | parent | next [-] |
| > For instance, in C, qsort() takes a function pointer for the comparison function, in Rust and C++, the standard library sorting functions are templated on the comparison function. That's more of a critique of the standard libraries than the languages themselves. If someone were writing C and cared, they could provide their own implementation of sort such that the callback could be inlined (LLVM can inline indirect calls when all call sites are known), just as it would be with C++'s std::sort. Further, if the libc allows for LTO (active area of research with llvm-libc), it should be possible to optimize calls to qsort this way. |
| |
| ▲ | jesse__ 6 hours ago | parent | next [-] | | "could" and "should" are doing some very theoretical heavy lifting here. Sure, at the limit, I agree with you, but in reality, relying on the compiler to do any optimization that you care about (such as inlining an indirect function call in a hot loop) is incredibly unwise. Invariably, in some cases it will fail, and it will fail silently. If you're writing performance critical code in any language, you give the compiler no choice in the matter, and do the optimization yourself. I do generally agree that in the case of qsort, it's an API design flaw | | |
| ▲ | oguz-ismail2 3 hours ago | parent [-] | | > qsort, it's an API design flaw It's just a generic sorting function. If you need more you're supposed to write it yourself. The C standard library exists for convenience not performance. | | |
| |
| ▲ | josephg 4 hours ago | parent | prev [-] | | > That's more of a critique of the standard libraries than the languages themselves. But we're right to criticise the standard libraries. If the average programmer uses standard libraries, then the average program will be affected (positively and negatively) by its performance and quirks. |
|
|
| ▲ | toodlemcnoodle 15 hours ago | parent | prev | next [-] |
| I agree with this whole-heartedly. Rust is a LANGUAGE and C is a LANGUAGE. They are used to describe behaviours. When you COMPILE and then RUN them you can measure speed, but that's dependent on two additional bits that are not intrinsically part of the languages themselves. Now: the languages may expose patterns that a compiler can make use of to improve optimizations. That IS interesting, but it is not a question of speed. It is a question of expressability. |
| |
| ▲ | pessimizer 15 hours ago | parent [-] | | No. As you've made clear, it's a question of being able to express things in a way that gives more information to a compiler, allowing it to create executables that run faster. Saying that a language is about "expressability" is obvious. A language is nothing other than a form of expression; no more, no less. | | |
| ▲ | toodlemcnoodle 15 hours ago | parent | next [-] | | Yes. But the speed is dependent on whether or not the compiler makes use of that information and the machine architecture the compiler is running it on. Speed is a function of all three -- not just the language. Optimizations for one architecture can lead to perverse behaviours on another (think cache misses and memory layout -- even PROGRAM layout can affect speed). These things are out of scope of the language and as engineers I think we ought to aim to be a bit more precise. At a coarse level I can understand and even would agree with something like "Python is slower than C", but the same argument applies there as well. But at some point objectivity ought to enter the playing field. | |
| ▲ | irishcoffee 15 hours ago | parent | prev [-] | | > ... it's a question of being able to express things in a way that gives more information to a compiler, allowing it to create executables that run faster. There is expressing idea via code, and there is optimization of code. They are different. Writing what one may think is "fully optimized code" the first time is a mistake, every time, and usually not possible for a codebase of any significant size unless you're a one-in-a-billion savant. Programming languages, like all languages, are expressive, but only as expressive as the author wants to be, or knows how to be. Rarely does one write code and think "if I'm not expressive enough in a way the compiler understands, my code might be slightly slower! Can't have that!" No, people write code that they think is correct, compile it, and run it. If your goal is to make the most perfect code you possibly can, instead of the 95% solution is the robust, reliable, maintainable, and testable, you're doing it wrong. Rust is starting to take up the same mental headspace as LLMs: they're both neat tools. That's it. I don't even mind people being excited about neat tools, because they're neat. The blinders about LLMs/Rust being silver bullets for the software industry need to go. They're just tools. |
|
|
|
| ▲ | jandrewrogers 15 hours ago | parent | prev | next [-] |
| The main performance difference between Rust, C, and C++ is the level of effort required to achieve it. Differences in level of effort between these languages will vary with both the type of code and the context. It is an argument about economics. I can write C that is as fast as C++. This requires many times more code that takes longer to write and longer to debug. While the results may be the same, I get far better performance from C++ per unit cost. Budgets of time and money ultimately determine the relative performance of software that actually ships, not the choice of language per se. I've done parallel C++ and Rust implementations of code. At least for the kind of performance-engineered software I write, the "unit cost of performance" in Rust is much better than C but still worse than C++. These relative costs depend on the kind of software you write. |
| |
| ▲ | pjmlp 14 hours ago | parent | next [-] | | > I can write C that is as fast as C++. Only if ignoring the C++ compile time execution capabilites. | | |
| ▲ | pmarin 14 hours ago | parent | next [-] | | C++ compile time execution is just a gimmicky code generator, you can do it in any language. | | |
| ▲ | pjmlp 13 hours ago | parent [-] | | Yeah, I could also be writting in a macro assembler for some Lisp inspired ideas and optimal performace. |
| |
| ▲ | jandrewrogers 13 hours ago | parent | prev [-] | | Any code that can be generated at compile-time can be written the old fashioned way. | | |
| ▲ | pjmlp 13 hours ago | parent [-] | | Including using a macro assembler with a bunch MASM/TASM like clever macros. |
|
| |
| ▲ | gf000 14 hours ago | parent | prev [-] | | > I can write C that is as fast as C++ I generally agree with your take, but I don't think C is in the same league as Rust or C++. C has absolutely terrible expressivity, you can't even have proper generic data structures. And something like small string optimization that is in standard C++ is basically impossible in C - it's not an effort question, it's a question of "are you even writing code, or assembly". | | |
| ▲ | jandrewrogers 14 hours ago | parent [-] | | Yes, it is the difference between "in theory" and "in practice". In practice, almost no one would write the C required to keep up with the expressiveness of modern C++. The difference in effort is too large to be worth even considering. It is why I stopped using C for most things. There is a similar argument around using "unsafe" in Rust. You need to use a lot of it in some cases to maintain performance parity with C++. Achievable in theory but a code base written in this way is probably going to be a poor experience for maintainers. Each of these languages has a "happy path" of applications where differences in expressivity will not have a material impact on the software produced. C has a tiny "happy path" compared to the other two. |
|
|
|
| ▲ | Measter 15 hours ago | parent | prev | next [-] |
| > On the other hand, signed integer overflow being UB would count for C/C++ C and C++ don't actually have an advantage here because this is only limited to signed integers unless you use compiler-specific intrinsics. Rust's standard library allows you to make overflow on any specific arithmetic operation UB on both signed and unsigned integers. |
| |
| ▲ | OskarS 15 hours ago | parent [-] | | It's interesting, because it's a "cultural" thing like the author discusses, it's a very good point. Sure, you can do unsafe integer arithmetic in Rust. And you can do safe integer arithmetic with overflow in C/C++. But in both cases, do you? Probably you don't in either case. "Culturally", C/C++ has opted for "unsafe-but-high-perf" everywhere, and Rust has "safe-but-slightly-lower-perf" everywhere, and you have to go out of your way to do it differently. Similarly with Zig and memory allocators: sure, you can do "dynamically dispatched stateful allocators that you pass to every function that allocates" in C, but do you? No, you probably don't, you probably just use malloc(). On the other hand: the author's point that the "culture of safety" and the borrow checker in Rust frees your hand to try some things in Rust which you might not in C/C++, and that leads to higher perf. I think that's very true in many cases. Again, the answer is more or less "basically no, all these languages are as fast as each other", but the interesting nuance is in what is natural to do as an experienced programmer in them. | | |
| ▲ | Xirdus 15 hours ago | parent [-] | | C++ isn't always "unsafe-but-high-perf". Move semantics are a good example. The spec goes to great lengths to ensure safety in a huge number of scenarios, at the cost of performance. Mostly shows up in two ways: one, unnecessary destructor calls on moved out objects, and two, allowing throwing exceptions in move constructors which prevents most optimizations that would be enabled by having move constructors in the first place (there was an article here recently on this topic). Another one is std::shared_ptr. It always uses atomic operations for reference counting and there's no way to disable that behavior or any alternative to use when you don't need thread safety. On the other hand, Rust has both non-atomic Rc and atomic Arc. | | |
| ▲ | josefx 13 hours ago | parent [-] | | > one, unnecessary destructor calls on moved out objects That issue predates move semantics by ages. The language always had very simple object life times, if you create Foo foo; it will call foo.~Foo() for you, even if you called ~Foo() before. Anything with more complex lifetimes either uses new or placement new behind the scenes. > Another one is std::shared_ptr. From what I understand shared_ptr doesn't care that much about performance because anyone using it to manage individual allocations already decided to take performance behind the shed to be shot, so they focused more on making it flexible. | | |
| ▲ | Xirdus 12 hours ago | parent [-] | | C++11 totally could have started skipping destructors for moved out values only. They chose not to, and part of the reason was safety. I don't agree with you about shared_ptr (it's very common to use it for a small number of large/collective allocations), but even if what you say is true, it's still a part of C++ that focuses on safety and ignores performance. Bottom line - C++ isn't always "unsafe-but-high-perf". |
|
|
|
|
|
| ▲ | marcosdumay 7 hours ago | parent | prev | next [-] |
| > On the other hand, signed integer overflow being UB would count for C/C++ Rust defaults to the platform treatment of overflows. So it should only make any difference if the compiler is using it to optimize your code, what will most likely lead to unintended behavior. |
| |
| ▲ | kibwen 5 hours ago | parent | next [-] | | Rust's overflow behavior isn't platform-dependent. By default, Rust panics on overflow when compiled in debug mode and wraps on overflow when compiled in release mode, and either behavior can be selected in either mode by a compiler flag. In neither case does Rust consider it UB for arithmetic operations to wrap. | |
| ▲ | astrange 7 hours ago | parent | prev [-] | | Writing a function with UB for overflows doesn't cause unintended behavior if you're doing it to signal there aren't any overflows. And it's very important because it's needed to do basically any loop rewriting. On the other hand, writing a function that recovers from overflows in an incorrect/useless way still isn't helpful if there are overflows. |
|
|
| ▲ | tick_tock_tick 5 hours ago | parent | prev | next [-] |
| You're qsort example is basically the same reason people say C++ is faster than Rust. C++ templates are still a lot more powerful than Rusts systems but that's getting closer and closer every day. |
| |
| ▲ | josephg 4 hours ago | parent [-] | | It is?? Can you give some examples of high performance stuff you can do using C++'s template system that you can't do in rust? | | |
| ▲ | jandrewrogers 34 minutes ago | parent [-] | | They are likely referring to the scope of fine-grained specialization and compile-time codegen that is possible in modern C++ via template metaprogramming. Some types of complex optimizations common in C++ are not really expressible in Rust because the generics and compile-time facilities are significantly more limited. As with C, there is nothing preventing anyone from writing all of that generated code by hand. It is just far more work and much less maintainable than e.g. using C++20. In practice, few people have the time or patience to generate this code manually so it doesn't get written. Effective optimization at scale is difficult without strong metaprogramming capabilities. This is an area of real strength for C++ compared to other systems languages. |
|
|
|
| ▲ | renox 16 hours ago | parent | prev | next [-] |
| >signed integer overflow being UB would count for C/C++ Then, I raise you to Zig which has unsigned integer overflow being UB. |
| |
| ▲ | steveklabnik 15 hours ago | parent | next [-] | | Interestingly enough, Zig does not use the same terminology as C/C++/Rust do here. Zig has "illegal behavior," which is either "safety checked" or "unchecked." Unchecked illegal behavior is like undefined behavior. Compiler flags and in-source annotations can change the semantics from checked to unchecked or vice versa. Anyway that's a long way of saying that you're right, integer overflow is illegal behavior, I just think it's interesting. | |
| ▲ | ladyanita22 15 hours ago | parent | prev [-] | | Rust has UB overflow as well, just unsafe. https://doc.rust-lang.org/std/intrinsics/fn.unchecked_add.ht... |
|
|
| ▲ | jarjoura 12 hours ago | parent | prev | next [-] |
| At that point the real question should be restated. Does the LLVM IL that is generated from clang and rustc matter in a meaningful way? |
|
| ▲ | dana321 16 hours ago | parent | prev | next [-] |
| Rust has linker optimizations that can make it faster in some cases |
| |
| ▲ | quotemstr 15 hours ago | parent [-] | | Huh? Both have LTO. There are linker optimizations available to Rust and not to C and C++. They all use the same God damn linker. | | |
| ▲ | josephg 4 hours ago | parent [-] | | A few years ago I pulled a rust library into a swift app on ios via static linking & C FFI. And I had a tiny bit of C code bridge the languages together. When I compiled the final binary, I ran llvm LTO across all 3 languages. That was incredibly cool. |
|
|
|
| ▲ | trueismywork 12 hours ago | parent | prev | next [-] |
| Strict aliasing analysis of rust will provide some fundamental better optimization than C. |
|
| ▲ | foldr 15 hours ago | parent | prev [-] |
| >in Rust and C++, the standard library sorting functions are templated on the comparison function. This means it's much easier for the compiler to specialize the sorting function, inline the comparisons and optimize around it. I think this is something of a myth. Typically, a C compiler can't inline the comparison function passed to qsort because libc is dynamically linked (so the code for qsort isn't available). But if you statically link libc and have LTO, or if you just paste the implementation of qsort into your module, then a compiler can inline qsort's comparison function just as easily as a C++ compiler can inline the comparator passed to std::sort. As for type-specific optimizations, these can generally be done just as well for a (void *) that's been cast to a T as they can be for a T (though you do miss out on the possibility of passing by value). That said, I think there is an indirect connection between a templated sort function and the ability to inline: it forces a compiler/linker architecture where the source code of the sort function is available to the compiler when it's generating code for calls to that function. |
| |
| ▲ | OskarS 15 hours ago | parent | next [-] | | qsort is obviously just an example, this situation applies to anything that takes a callback: in C++/Rust, that's almost always generic and the compiler will monomorphize the function and optimize around it, and in C it's almost always a function pointer and a userData argument for state passed on the stack. (and, of course, it applies not just to callbacks, but more broadly to anything templated). I'm actually very curious about how good C compilers are at specializing situations like this, I don't actually know. In the vast majority cases, the C compiler will not have access to the code (either because of dynamic linking like in this example, or because the definition is in another translation unit), but what if it does? Either with static linking and LTO, or because the function is marked "inline" in a header? Will C compilers specialize as aggressively as Rust and C++ are forced to do? If anyone has any resources that have looked into this, I would be curious to hear about it. | | |
| ▲ | Maxatar 11 hours ago | parent | next [-] | | Dynamic linking will inhibit inlining entirely, and so yes qsort does not in practice get inlined if libc is dynamically linked. However, compilers can inline definitions across translation units without much of any issue if whole program optimization is enabled. The use of function pointers doesn't have much of an impact on inlining. If the argument supplied as a parameter is known at compile time then the compiler has no issue performing the direct substitution whether it's a function pointer or otherwise. | |
| ▲ | foldr 13 hours ago | parent | prev [-] | | My point is that the real issue is just whether or not the function call is compiled as part of the same unit as the function. If it is, then, certainly, modern C compilers can inline functions called via function pointers. The inlining itself is not made easier via the template magic. Your C comparator function is already “monomirphized” - it’s just not type safe. |
| |
| ▲ | jarjoura 12 hours ago | parent | prev [-] | | Wouldn't C++ and Rust eventually call down into those same libc functions? I guess for your example, qsort() it is optional, and you can chose another implementation of that. Though I tend to find that both standard libraries tend to just delegate those lowest level calls to the posix API. | | |
| ▲ | steveklabnik 12 hours ago | parent | next [-] | | Rust doesn't call into libc for sort, it has its own implementation in the standard library. | |
| ▲ | adgjlsfhk1 7 hours ago | parent | prev [-] | | Many of the libc functions are bad apis with traditionally bad implementations. |
|
|