Piotr Kołaczkowski, a Cassandra and Rust developer, writes about performance benchmarks, concurrency, memory use and disk I/O in Rust and Java.
HN commenters discuss his benchmarks and corrections on Arc overhead and Java memory use, and link his earlier async memory comparison and his latte benchmarking tool.
> Does https://github.com/pkolaczk/latte count?
This is just Rust code. Where is the equivalent Java code?
Like the 50x-100x memory consumption is highly suspicious. If Java uses 50x the memory consumption compare to C++, how come I can allocate a long[SINT_MAX-10] on a system machine with 32 Gb of RAM? Shouldn't the process require of order 0.8 Tb of RAM if this statement is correct? Or can C++ allocate a 2 bn array of longs in 40 Mb of RAM? If so let me know, I would be very interested in using this novel compression technology.
> Or the Optional cost described on my blog here: https://pkolaczk.github.io/overhead-of-optional/?
Why aren't you using OptionalLong[1]? You shouldn't use Optional<Long>, that's never a good choice. At any rate, nobody should be claiming Java optionals are are free, they're a high level abstraction and absolutely do not belong in hot codepaths.
In general it's fairly easy to construct benchmarks that favor any particular language, which is why you constantly see these blog posts about how high level interpreted languages (JS, PHP, Haskell) are faster than C++.
You can easily construct "comparisons" that make JVM languages look superior to C++ as well, just carelessly allocate throwaway objects of different sizes and lifetimes (like you can in Java), oh no, why is C++ slowing down? Surely there's no heap fragmentation! That's a bad faith benchmark though. It doesn't really demonstrate anything other than that C++ following Java idioms isn't…
> Thanks for the answer. But is this actual behaviour for the GCs of the JDK? I was certain that at the very least Hotspot makes use of stack allocation as much as possible.
Not really, java has some escape analysis but it's very limited in its ability to stack allocate as it can't put entire structures (objects) on the stack, it only works if the compiler manages to scalar-replace the object (https://shipilev.net/jvm/anatomy-quarks/18-scalar-replacement/) and that has somewhat restricted applicability (https://pkolaczk.github.io/overhead-of-optional/). The behaviour I'm talking about is mostly that of Go, as it is much more capable of stack allocating, and specifically has a non-generational GC because in testing they found generational GCs had a very variable impact depending on workload (rather than a universally or near universally positive impact).
Would an atomic mutable subclass cache (not sure what it's used for, downcasting?) be unnecessary in a language built around static rather than dynamic dispatch by default, like C/C++/Rust and perhaps Go? Or would it still speed up dynamically dispatched code, but is less practical or worthwhile so it isn't used in practice? (Though Rust's Arc also suffers from atomic contention similar to this blog post, when used across dozens of threads: https://pkolaczk.github.io/server-slower-than-a-laptop/)
Also it's somewhat ironic that the JVM source code (https://github.com/openjdk/jdk8u/blob/jdk8u352-b07/hotspot/src/cpu/x86/vm/macroAssembler_x86.cpp#L5093) says "This code is rarely used, so simplicity is a virtue here" at the site of a bottleneck.
It is in the original article [1], though I was made aware that Go implementation is also incorrect as `defer` in it make the memory usage to substantially raise. So instead of:
defer wg.Done()
time.Sleep(10 * time.Second)
In original article it should be:
time.Sleep(10 * time.Second)
wg.Done()
And memory usage will drop about 2x. I am no Go nor Java developer so I cannot say anything more.
[1]: https://pkolaczk.github.io/memory-consumption-of-async/
On Linux, 400k threads was a thing on even on 2002 hardware that was 32-bit and thus limited to 4GB: https://news.ycombinator.com/item?id=37621887
In the titular post there's a link to a previous comparison between approaches, and plain OS threads used from Rust fare quite well, even if the author doesn't up the OS limits to keep that in the running for the higher thread cases: https://pkolaczk.github.io/memory-consumption-of-async/
The "if I then go and code in Python, Java or C#, pretty much all objects have the overhead of an Arc" is not accurate. Rust Arc involves atomic operation and its preformance can greatly degrade when the reference count is being mutated by many threads. See https://pkolaczk.github.io/server-slower-than-a-laptop/
Java, C# and Go don't use atomic reference counting and don't have such overhead.