| > A lot of our C++ code is intrinsics (including memory primitives like _mm_stream_ps and barriers) 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. |
The only thing that was different was that we had a number of platform-specific intrinsics to really shake fast code out. E.g. shuffles on x86 on older SSE editions were terrible and we would have custom x86 code for shuffles or let memory out differently.
The only thing we use from C++ stdlib is unique_ptr. For everything else we had our own much more tailored, much faster, stuff. We had something like 10 different array containers for example.
Yeah what you described with templates is what we are doing re speculative optimisation. We have tuned versions for different workloads. We would inspect before we decide which one to run (only if that wasn't slower then just having one implementation, which was often the case because of instruction cache).
Something to be aware of is that on consoles mmapping a page to be executable was forbidden. So no JIT. And you aim for your slowest target so PC just follows that.