Comment by palata

3 months ago

I really like Rust as a replacement for C++, especially given that C++ seems to become crazier every year. When reasonable, nowadays I always use Rust instead of C++.

But for the vast majority of projects, I believe that C++ is not the right language, meaning that Rust isn't, either.

I feel like many people choose Rust because is sounds like it's more efficient, a bit as if people went for C++ instead of a JVM language "because the JVM is slow" (spoiler: it is not) or for C instead of C++ because "it's faster" (spoiler: it probably doesn't matter for your project).

It's a bit like choosing Gentoo "because it's faster" (or worse, because it "sounds cool"). If that's the only reason, it's probably a bad choice (disclaimer: I use and love Gentoo).

I have a personal-use app that has a hot loop that (after extensive optimization) runs for about a minute on a low-powered VPS to compute a result. I started in Java and then optimized the heck out of it with the JVM's (and IntelliJ's) excellent profiling tools. It took one day to eliminate all excess allocations. When I was confident I couldn't optimize the algorithm any further on the JVM I realized that what I'd boiled it down to looked an awful lot like Rust code, so I thought why not, let's rewrite it in Rust. I took another day to rewrite it all.

The result was not statistically different in performance than my Java implementation. Each took the same amount of time to complete. This surprised me, so I made triply sure that I was using the right optimization settings.

Lesson learned: Java is easy to get started with out of the box, memory safe, battle tested, and the powerful JIT means that if warmup times are a negligible factor in your usage patterns your Java code can later be optimized to be equivalent in performance to a Rust implementation.

  • I wrote a few benchmarks a few years ago comparing JS vs C++ compiled to WASM vs C++ compiled to x64 with -O3.

    I was surprised that the heaviest one (a lot of float math) run about the same speed in JS vs C++ -> x64. The code was several nested for loops manipulating a buffer and using only local-scoped variables and built-in Math library functions (like sqrt) with no JS objects/arrays besides the buffer. So the code of both implementations was actually very similar.

    The C++ -> WASM version of that one benchmark was actually significantly slower than both the JS and C++ -> x64 version (again, a few years ago, I imagine it got better now).

    Most compilers are really good at optimizing code if you don't use the weird "productivity features" of your higher level languages. The main difference of using lower level languages is that not being allowed to use those productivity features prevents you from accidentally tanking performance without noticing.

    I still hope to see the day where a language could have multiple "running modes" where you can make an individual module/function compile with a different feature-set for guaranteeing higher performance. The closest thing we have to this today is Zig using custom allocators (where opting out of receiving an allocator means no heap allocations are guaranteed for the rest of the stack call) and @setRuntimeSafety(false) which disables runtime safety checks (when using ReleseSafe compilation target) for a single scope.

  • I'd rather write rust than java, personally

    • If I have all the time in the world, sure. When I'm racing against a deadline, I don't want to wrestle with the borrow checker too. Sure, it's objections help with the long term quality of the code and reduce bugs but that's hard to justify to a manager/process driven by Agile and Sprints. Quite possible that an experienced Rust dev can be very productive but there aren't tons of those going around.

      Java has the stigma of ClassFactoryGeneratorFactory sticking to it like a nasty smell but that's not how the language makes you write things. I write Java professionally and it is as readable as any other language. You can write clean, straightforward and easy to reason code without much friction. It's a great general purpose language.

      23 replies →

    • I'd have said the same thing 10 years ago (or, I would have if I were comparing 10-year-old Java with modern Rust), but Java these days is actually pretty ergonomic. Rust's borrow checker balances out the ML-style niceties to bring it down to about Java's level for me, depending on the application.

  • >I realized that what I'd boiled it down to looked an awful lot like Rust code

    you're no longer writing idiomatic java at this point - probably with zero object oriented programming. so might as well write it in Rust from the get-go.

    • If I'd started in Rust I likely wouldn't have finished it at all. Java allowed me to start out just focused on the algorithm with very little regard for memory usage patterns and then refactor towards zero garbage collection. Rust can sort of allow the same thing by just sprinkling everything with clone and/or Rc/Arc, but it's much more in the way than just having a garage collector there automatically.

I write a lot of Rust, but as you say, it's basically a vastly improved version of C++. C++ is not always the right move!

For all my personal projects, I use a mix of Haskell and Rust, which I find covers 99% of the product domains I work in.

Ultra-low level (FPGA gateware): Haskell. The Clash compiler backend lets you compile (non-recursive) Haskell code directly to FPGA. I use this for audio codecs, IO expanders, and other gateware stuff.

Very low-level (MMUless microcontroller hard-realtime) to medium-level (graphics code, audio code): Rust dominates here

High-level (have an MMU, OS, and desktop levels of RAM, not sensitive to ~0.1ms GC pauses): Haskell becomes a lot easier to productively crank out "business logic" without worrying about memory management. If you need to specify high-level logic, implement a web server, etc. it's more productive than Rust for that type of thing.

Both languages have a lot of conceptual overlap (ADTs, constrained parametric types, etc.), so being familiar with one provides some degree of cross-training for the other.

  • What do you mean by 'a mix of Haskell and Rust'? Is that a per-project choice or do you use both in a single project? I'm interested in the latter. If so, could you point me to an example?

    Another question is about Clash. Your description sounds like the HLS (high level synthesis) approach. But I thought that Clash used a Haskell -based DSL, making it a true HDL. Could you clarify this? Thanks!

    • Yeah, sometimes I'll do e.g. a backend state management server in Haskell and then a lightweight (embedded) client in Rust. I haven't ever tried linking rust from haskell yet, if that's what you mean.

      I would actually flip your HDL definition a bit. Clash is a true HDL, but specifically because it's not just a shallowly embedded DSL.

      Clash is actually a GHC plugin that compiles haskell code to a synchronous circuit representation and then can spit that out as whatever HDL you like. It's emphatically not just a library for constructing circuit descriptions, like most new gateware development tools. This is possible because the semantics of Haskell are (by a mixture of good first-principles design and luck) an almost exact match for the standard four-state logic semantics of synchronous digital circuits.

      This is also different from the standard HLS approach where the semantics of the source language do not at all match the semantics of the target. With Haskell, they are (surprisingly) very close! Only in a few edge cases (mostly having to do with zero-bit wires and so on) does the evaluation semantics of haskell differ from the evaluation semantics of e.g. verilog.

> "I really like Rust as a replacement for C++, especially given that C++ seems to become crazier every year."

I don't understand this argument, which I've also seen it used against C#, quite frequently. When a language offers new features, you're not forced to use them. You generally don't even need to learn them if you don't want. I do think some restrictions in languages can be highly beneficial, like strong typing, but the difference is that in a weakly typed language that 'feature' is forced upon you, whereas random new feature in C++ or C# is near to always backwards compatible and opt-in only.

For instance, to take a dated example - consider move semantics in C++. If you never used it anywhere at all, you'd have 0 problems. But once you do, you get lots of neat things for free. And for these sort of features, I see no reason to ever oppose their endless introduction unless such starts to imperil the integrity/performance of the compiler, but that clearly is not happening.

  • You can't avoid a lot of this stuff, once libraries start using it or colleagues add it to your codebase then you need to know it. I'd argue you need to know it well before you decide to exclude it.

  • Then better be quite picky of what libraries one choses, because that is the thing, while we may not use them, the libraries migth impose them on us.

    Same applies having to deal with old features, replaced by modern ways, old codebases don't get magically rewritten, and someone has to understand modern and old ways.

    Likewise I am not a big fan of C and Go, as visible by my comment history, yet I know them well enough, because in theory I am not forced to use them, in practice, there are business contexts where I do have to use them.

  • My experience with C++ is that it fundamentally "looks worse" and has worse tooling than more modern languages. And it feels like they keep adding new features that make it all even worse every year.

    Sure, you don't have to use them, but you have to understand them when used in libraries you depend on. And in my experience in an environment of C++ developers, many times you end up having some colleagues who are very vocal about how you should love the language and use all the new features. Not that this wouldn't happen in Java or Kotlin, but the fact is that new features in those languages actually improve the experience with the language.

    • I'm a C++ developer and it's always great when we move to a newer language version, with all the language improvements that come with that.

>> a bit as if people went for C++ instead of a JVM language "because the JVM is slow" (spoiler: it is not)

The OP is doing game development. It’s possible to write a performant game in Java but you end up fighting the garbage collector the whole way and can’t use much library code because it’s just not written for predictable performance.

VPS/Cloud providers skimp on RAM. The JVM sucks for any low RAM workload, where you want the smallest possible single server instance. The startup times of JVM based applications are also horrendous. How many gigabytes of RAM does Digital Ocean give you with your smallest instance? They don't. They give you 512MiB. Suddenly using Java is no longer an option, because you will be wasting your day carefully tuning literally everything to fit in that amount.

  • You can get decent startup times if you have fewer dependencies. The JVM itself starts fairly quickly (<200 ms), the problem is all the class loading. If your "app" is a bloated multi gigabyte monstrosity... good luck!

I think the choice of C++ vs JVM depends on your project. If you're not using the benefits of "unsafe" languages then it probably doesn't matter.

But if you are after performance how do do the following in Java? - Build an AOS so that memory access is linear re cache. Prefetch. Use things like _mm_stream_ps() to tell the CPU the cache line you're writing to doesn't need to be fetched. Share a buffer of memory between processes by atomic incrementing a head pointer.

I'm pretty sure you could build an indie game without low-level C++, but there is a reason that commercial gamedev is typically C++.

  • While there are many technical reasons to use C++ over Java in game development, many commercial games could be easily done in Java, as they are A or AA level at most.

    Had Notch thought too much about which language to use, maybe he would still be trying to launch a game today.

  • > but there is a reason that commercial gamedev is typically C++.

    Sure, and that's kind of my point. There are a few use-cases where C++ is actually needed, and for those cases, Rust (the language) is a good alternative if it's possible to use it.

    But even for gamedev, the article here says that they moved to Unity. The core of Unity is apparently C++, but users of Unity code in C#. Which kind of proves my point: outside of that core that actually needs C++, it doesn't matter much. And the vast majority of software development is done outside of those core use-cases, meaning that the vast majority of developers do not need Rust.

    • We were using a modified Luajit, in assembly, with a bit of other assembly dotted around the place. That assembly takes a long time to write (to beat a modern C++ compiler).

      Then we had C++ for all our low level code and Lua for gameplay.

      We were floating a middle layer of Rust for Lua bindings and the glue code for our transformation pipeline, but there was always a little too much friction to introduce. What we were particularly interested in was memory allocation bugs (use after free and leaks) and speeding up development of the engine. So I could see it having a place.

Rust is very easy when you want to do easy things. You can actually just completely avoid the borrow-checker altogether if you want to. Just .clone(), or Arc/Mutex. It's what all the other languages (like Go or Java) are doing anyway.

But if you want to do a difficult and complicated thing, then Rust is going to raise the guard rails. Your program won't even compile if it's unsafe. It won't let you make a buggy app. So now you need to back up and decide if you want it to be easy, or you want it to be correct.

Yes, Rust is hard. But it doesn't have to be if you don't want.

  • This argument goes only so far. Would you consider querying a database hard? Most developers would say no. But it’s actually a pretty hard problem, if you want to do it safely. In rust, that difficultly leaks into the crates. I have a project that uses diesel and to make even a single composable query is a tangle of uppercase Type soup.

    This just isn’t a problem in other languages I’ve used, which granted aren’t as safe.

    I love Rust. But saying it’s only hard if you are doing hard things is an oversimplification.

    • Building a proper ORM is hard. Querying a database is not. See the postgres crate for an example.

      Querying a database while ensuring type safety is harder, but you still don't need an OEM for that. See sqlx.

      3 replies →

    • > This just isn’t a problem in other languages I’ve used, which granted aren’t as safe.

      Most languages used with DBs are just as safe. This idea about Rust being more safe than languages with GC needs a rather big [Citation Needed] sign for the fans.

  • If you use Rust with `.clone()` and Arc/Mutex, why not just using one of the myriad of other modern and memory safe languages like Go, Scala/Kotlin/Java, C#, Swift?

    The whole point of Rust is to bring memory safety with zero cost abstraction. It's essentially bringing memory safety to the use-cases that require C/C++. If you don't require that, then a whole world of modern languages becomes available :-).

    • For me personally, doing the clone-everything style of Rust for a first pass means I still have a graceful incremental path to go pursue the harder optimizations that are possible with more thoughtful memory management. The distinction is that I can do this optimization pass continuing to work in Rust rather than considering, and probably discarding, a potential rewrite to a net-new language if I had started in something like Ruby/Python/Elixir. FFI to optimize just the hot paths in a multi-language project has significant downsides and tradeoffs.

      Plus in the meantime, even if I'm doing the "easy mode" approach I get to use all of the features I enjoy about writing in Rust - generics, macros, sum types, pattern matching, Result/Option types. Many of these can't be found all together in a single managed/GC'd languages, and the list of those that I would consider viable for my personal or professional use is quite sparse.

      5 replies →

This couldn't be any more accurate even if you compiled with CFLAGS='-march native ' and RUSTFLAGS='-C can't remember insert here'

Install Gentoo

  • As I said, I use Gentoo already ;-).

    • Quite.

      I was a Gentoo user (daily driver) for around 15 years but the endless compilation cycles finally got to me. It is such a shame because as I started to depart, Gentoo really got its arse in gear with things like user patching etc and no doubt is even better.

      It has literally (lol) just occurred to me that some sort of dual partition thing could sort out my main issue with Gentoo.

      @system could have two partitions - the running one and the next one that is compiled for and then switched over to on a reboot. @world probably ought to be split up into bits that can survive their libs being overwritten with new ones and those that can't.

      Errrm, sorry, I seem to have subverted this thread.

      3 replies →

> C instead of C++ because "it's faster" (spoiler: it probably doesn't matter for your project)

If your C is faster than your C++ then something has gone horribly wrong. C++ has been faster than C for a long time. C++ is about as fast as it gets for a systems language.

  • > C++ has been faster than C for a long time.

    What is your basis for this claim? C and C++ are both built on essentially the same memory and execution model. There is a significant set of programs that are valid C and C++ both -- surely you're not suggesting that merely compiling them as C++ will make them faster?

    There's basically no performance technique available in C++ that is not also available in C. I don't think it's meaningful to call one faster than the other.

    • This is really an “in theory” versus “in practice” argument.

      Yes, you can write most things in modern C++ in roughly equivalent C with enough code, complexity, and effort. However, the disparate economics are so lopsided that almost no one ever writes the equivalent C in complex systems. At some point, the development cost is too high due to the limitations of the expressiveness and abstractions. Everyone has a finite budget.

      I’ve written the same kinds of systems I write now in both C and modern C++. The C equivalent versions require several times the code of C++, are less safe, and are more difficult to maintain. I like C and wrote it for a long time but the demands of modern systems software are a beyond what it can efficiently express. Trying to make it work requires cutting a lot of corners in the implementation in practice. It is still suited to more classically simple systems software, though I really like what Zig is doing in that space.

      I used to have a lot of nostalgia for working in C99 but C++ improved so rapidly that around C++17 I kind of lost interest in it.

      1 reply →

    • > What is your basis for this claim?

      Great question! Here's one answer:

      Having written a great deal of C code, I made a discovery about it. The first algorithm and data structure selected for a C program, stayed there. It survives all the optimizations, refactorings and improvements. But everyone knows that finding a better algorithm and data structure is where the big wins are.

      Why doesn't that happen with C code?

      C code is not plastic. It is brittle. It does not bend, it breaks.

      This is because C is a low level language that lacks higher level constructs and metaprogramming. (Yes, you can metaprogram with the C preprocessor, a technique right out of hell.) The implementation details of the algorithm and data structure are distributed throughout the code, and restructuring that is just too hard. So it doesn't happen.

      A simple example:

      Change a value to a pointer to a value. Now you have to go through your entire program changing dots to arrows, and sprinkle stars everywhere. Ick.

      Or let's change a linked list to an array. Aarrgghh again.

      Higher level features, like what C++ and D have, make this sort of thing vastly simpler. (D does it better than C++, as a dot serves both value and pointer uses.) And so algorithms and data structures can be quickly modified and tried out, resulting in faster code. A traversal of an array can be changed to a traversal of a linked list, a hash table, a binary tree, all without changing the traversal code at all.

    • C and C++ do have very different memory models, C essentially follows the "types are a way to decode memory" model while C++ has an actual object model where accessing memory using the wrong type is UB and objects have actual lifetimes. Not that this would necessarily lead to performance differences.

      When people claim C++ to be faster than C, that is usually understood as C++ provides tools that makes writing fast code easier than C, not that the fastest possible implementation in C++ is faster than the fastest possible implementation in C, which is trivially false as in both cases the fastest possible implementation is the same unmaintainable soup of inline assembly.

      The typical example used to claim C++ is faster than C is sorting, where C due to its lack of templates and overloading needs `qsort` to work with void pointers and a pointer to function, making it very hard on the optimiser, when C++'s `std::sort` gets the actual types it works on and can directly inline the comparator, making the optimiser work easier.

      19 replies →

    • I know you're going to reply with "BUT MY PREPROCESSOR", but template specialization is a big win and improvement (see qsort vs std::sort).

      5 replies →

  • > If your C is faster than your C++ then something has gone horribly wrong. C++ has been faster than C for a long time.

    In certain cases, sure - inlining potential is far greater in C++ than in C.

    For idiomatic C++ code that doesn't do any special inlining, probably not.

    IOW, you can rework fairly readable C++ code to be much faster by making an unreadable mess of it. You can do that for any language (C included).

    But what we are usually talking about when comparing runtime performance in production code is the idiomatic code, because that's how we wrote it. We didn't write our code to resemble the programs from the language benchmark game.

  • I doubt that because C++ encourages heavy use of dynamic memory allocations and data structures with external nodes. C encourages intrusive data structures, which eliminates many of the dynamic memory allocations done in C++. You can do intrusive data structures in C++ too, but it clashes with object oriented idea of encapsulation, since an intrusive data structure touches fields of the objects inside it. I have never heard of someone modifying a class definition just to add objects of that class to a linked list for example, yet that is what is needed if you want to use intrusive data structures.

    While I do not doubt some C++ code uses intrusive data structures, I doubt very much of it does. Meanwhile, C code using <sys/queue.h> uses intrusive lists as if they were second nature. C code using <sys/tree.h> from libbsd uses intrusive trees as if they were second nature. There is also the intrusive AVL trees from libuutil on systems that use ZFS and there are plenty of other options for such trees, as they are the default way of doing things in C. In any case, you see these intrusive data structures used all over C code and every time one is used, it is a performance win over the idiomatic C++ way of doing things, since it skips an allocation that C++ would otherwise do.

    The use of intrusive data structures also can speed up operations on data structures in ways that are simply not possible with idiomatic C++. If you place the node and key in the same cache line, you can get two memory fetches for the price of one when sorting and searching. You might even see decent performance even if they are not in the same cache line, since the hardware prefetcher can predict the second memory access when the key and node are in the same object, while the extra memory access to access a key in a C++ STL data structure is unpredictable because it goes to an entirely different place in memory.

    You could say if you have the C++ STL allocate the objects, you can avoid this, but you can only do that for 1 data structure. If you want the object to be in multiple data structures (which is extremely common in C code that I have seen), you are back to inefficient search/traversal. Your object lifetime also becomes tied to that data structure, so you must be certain in advance that you will never want to use it outside of that data structure or else you must do at a minimum, another memory allocation and some copies, that are completely unnecessary in C.

    Exception handling in C++ also can silently kill performance if you have many exceptions thrown and the code handles it without saying a thing. By not having exception handling, C code avoids this pitfall.

    • OO (implementation inheritance) is frowned upon in modern C++. Also, all production code bases I’ve seen pass -fno-exceptions to the compiler.

      2 replies →

  • > If your C is faster than your C++ then something has gone horribly wrong. C++ has been faster than C for a long time. C++ is about as fast as it gets for a systems language.

    That's interesting, did ChatGPT tell you this?

I agree with you except for the JVM bit - but everyone's application varies

  • My point is that there are situations where C++ (or Rust) is required because the JVM wouldn't work, but those are niche.

    In my experience, most people who don't want a JVM language "because it is slow" tend to take this as a principle, and when you ask why their first answer is "because it's interpreted". I would say they are stuck in the 90s, but probably they just don't know and repeat something they have heard.

    Similar to someone who would say "I use Gentoo because Ubuntu sucks: it is super slow". I have many reasons to like Gentoo better than Ubuntu as my main distro, but speed isn't one in almost all cases.

    • The JVM is excellent for throughput, once the program has warmed up, but it always has much more jitter than a more systemsy language like C++ or Rust. There are definitely use cases where you need to consistently react fast, where Java is not a good choice.

      It also struggles with numeric work involving large matrices, because there isn't good support for that built into the language or standard library, and there isn't a well-developed library like NumPy to reach for.

      1 reply →

Rust is actually quite suitable for a number of domains where it was never intended to excel.

Writing web service backends is one domain where Rust absolutely kicks ass. I would choose Rust/(Actix or Axum) over Go or Flask any day. The database story is a little rough around the edges, but it's getting better and SQLx is good enough for me.

edit: The downvoters are missing out.

  • To me, web dev really sounds like the one place where everything works and it's more a question of what is in fashion. Java, Ruby, Python, PHP, C, C++, Go, Rust, Scala, Kotlin, probably even Swift? And of course NodeJS was made for that, right?

    I am absolutely convinced I can find success story of web backends built with all those languages.

    • Yeah, "web services backend" really means "code exercising APIs pioneered by SunOS in 1988". It's easy to be rock solid if your only dependency is the bedrock.

    • > probably even Swift

      It's possible to write web backends in Swift, but it's probably not a good idea. When I last did so, I ran into ridiculous issues all the time, such as lazy variables not being thread-safe, secondary threads having ridiculously small (and non-adjustable) stack sizes and there being generally absolutely no story for failure recovery (have fun losing all your other in-flight requests and restarting your app in case of an invalid array access). It's possible that some of this has been fixed in the last 5 years since I stopped working on that project, but given Apple's priorities, I somehow doubt that the situation is significantly better.

    • The bar for web services is low, so pretty much anything works as long as it's easy. I wouldn't call them a success story.

      When things get complex, you start missing Rust's type system and bugs creep in.

      In node.js there was a notable improvement when TS became the de-facto standard and API development improved significantly (if you ignore the poor tooling, transpiling, building, TS being too slow). It's still far from perfect because TS has too many escape hatches and you can't trust TS code; with Rust, if it compiles and there are no unsafe (which is rarely a problem in web services) you get a lot of compile time guarantees for free.

    • There are 3 cases. The first is that you are comfortable with Rust and you just choose it for that. The second is that you're not comfortable with Rust and you choose something else that works for you.

      The third is the interesting one. When your service has a lot of traffic and every bit of inefficiency costs you money (node rents) and energy. Rust is an obvious improvement over the interpreted languages. There are also a few rare cases where Rust has enough advantages over Go to choose the former. In general though, I feel that a lot of energy consumption and emissions can be avoided by choosing an appropriate language like Rust and Go.

      This would be a strong argument in favor of these languages in the current environmental conditions, if it weren't for 'AI'. Whether it be to train them or run them, they guzzle energy even for problems that could be solved with a search engine. I agree that LLMs can do much more. But I don't think they do enough for the energy they consume.

      8 replies →

    • Perhaps. But a comparable Rust backend stack produces a single binary deployable that can absorb 50,000 QPS with no latency caused by garbage collection. You get all of that for free.

      The type system and package manager are a delight, and writing with sum types results in code that is measurably more defect free than languages with nulls.

      16 replies →