|
|
|
|
|
by mabster
8 days ago
|
|
I'm a Java developer now, amongst other languages. The advantage of Java is that it takes A LOT less time to develop something, so there is the whole bang for buck for sure. I have had a few problems where I would love shared direct memory access and some atomics (because it would be a lot easier). But for the most part developing in Java is a lot quicker. I don't think game developers are more conservative than any other developers. We do have large C++ codebases and so it's hard to change. All modern engines have a few scripting languages tacked on too. Something like Lua usually is the sweet spot: most of the people developing scripts are not developers. We even had a Java interpreter for scripting once, but it lost favor for this reason. There were exceptions, but I found that developers generally preferred C# over Java anyway. Our assets pipelines are generally in C# already. Any speculative optimisation we were doing by hand. There is the whole deferring allocations / moving allocations, both of which we were already doing (e.g. copying every frame). A lot of our C++ code is intrinsics (including memory primitives like _mm_stream_ps and barriers) and you HAVE to have good control over how memory is laid out (e.g. knowing that data is split between cache lines so that you you don't get contention). Lots of spin locks too. I just don't see how you can do this kind of low level work in Java. |
|
Java has such intrinsicts, too: https://docs.oracle.com/en/java/javase/25/docs/api/java.base.... They may not look like intrinsics that compile to a single machine instruction, but the are (I don't think we offer stream access, simply because there hasn't been demand for it; if there is, we can add it. I actually added a streaming array copy to the JVM because I thought I could use it for something, but the results weren't what I expected, so I took it out)
BTW, here's a list of our intrinsics:
https://github.com/openjdk/jdk/blob/master/src/hotspot/share...
As you might notice, they include SIMD intrinsics offered through https://docs.oracle.com/en/java/javase/25/docs/api/jdk.incub...
> and you HAVE to have good control over how memory is laid out (e.g. knowing that data is split between cache lines so that you don't get contention)
We have the `@Contended` annotation precisely for that: https://github.com/openjdk/jdk/blob/master/src/java.base/sha... You have to use a flag to tell the JVM to respect this annotation, but the people who write high performance code know this: https://www.baeldung.com/java-false-sharing-contended
> Lots of spin locks too.
We have an intrinsic for spin locks: Thread.onSpinWait() https://docs.oracle.com/en/java/javase/25/docs/api/java.base...()
> I just don't see how you can do this kind of low level work in Java.
There's no reason you should if you're not writing high performance code in Java, but the people who write such code in Java know how to do these things in Java.
To be clear, Java certainly doesn't offer as much precise control as a low-level language, but it does offer everything you need for high performance (except array-of-struct, but that will arrive soon). The reason for that is that there's high demand for these constructs because so much of the worlds performance-sensitive software is written in Java. Traditionally, not games (which often have to run on platforms for which we don't offer Java) but manufacturing automation, defence, and trading.
> There is the whole deferring allocations / moving allocations, both of which we were already doing (e.g. copying every frame).
Yes, you can certainly do some memory management optimisations in C++, although with some effort (it's especially hard to use some standard library stuff, but when I write high performance code in C++ I don't use std at all). The low-level language that makes it easier is Zig.
> Any speculative optimisation we were doing by hand.
It's hard to do speculative optimisation by hand, unless you're generating code on the fly. The way speculative optimisations work is that we observe that something has been true so far (e.g. think about a specific branch that's always taken or a dynamic dispatch that only hits a certain target at a certain callsite) but the compiler can't prove that it's necessarily true. So we emit machine code that assumes it's true with special traps that would trigger some fault signal if the assumption is invalidated. If the trap is hit, we capture the signal, deoptimise the subroutine and then recompile it differently (without the assumption).
In C++ what I do is do some of the same optimisation results by hand (typically using templates), but of course, they're not speculative and I need to be careful. There's also code size and I-cache implications, but while we try to keep an eye on the I-cache, Java doesn't always get this balance right, either.