Comment by lolinder
4 months ago
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've also seen Cython used to this effect for hotspots or entire applications in scientific Python code.
I am not super familiar with python but that sounds quite annoying to set up in the build process. You would need to compile different files/modules using a different compiler right?
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.
Java is incredibly productive - it's fast and has the best tooling out there IMO.
Unfortunately it's not a good gaming language. GC pauses aren't really acceptable (which C# also suffers from) and GPU support is limited.
Miguel de Icaza probably has more experience than anyone building game engines on GC platforms and is very vocally moving toward reference counted languages [1]
[1] https://www.youtube.com/watch?v=tzt36EGKEZo
17 replies →
I have found that the ClassFactoryGeneratorFactories sneak up on you. Even if you don't want to the ecosystem slowly but surely nudges you that way.
4 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’d rather write Java than Rust, personally
Same here, and if I get bored with Java, there is also Scala, Kotlin and Clojure to chose from.
However, I would still prefer C# or F#.
Hence why I enjoy both stacks, lots of goodies to chose from, with great tooling.
2 replies →
Wow, way to be un-hip.
Note that I mentioned JVM languages. There is Scala, Kotlin and others. Kotlin is the default for Android, and it is really nice.
Kotlin is nice indeed. Most of the issues I had with it were in interop with Java code (those pesky platform types, that behave like non-nullable but are nullable: and you are back in the NPE swamp!)
>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.
Yes but it would just be the hot loop in this case; the rest of the app can still be in idiomatic Java, and you still get the GC.
Exactly. Write it in Java, optimize what you need to, leave the rest alone.
1 reply →