With RISC-V being as slow as it is, where is the use beyond proof of concept?
Impressive. I would imagine it is very difficult to get native performance without ISA extensions like Apple had to do for TSO and other awkward x86isms.
I would guess someone will make a RISC-V extension for that stuff eventually, though I haven’t seen anyone propose one yet.
puts entirety of x86_64 into a RISCV ISA extension
That was a fascinating read. Thank you. I suppose it is possible that there could be a RISC-V extension for this.
As the article states though, this is an ancient x86 artifact not often used in modern x86-64 software. If the code generated by GCC and Clang does not create such code, it may not exist in Linux binaries in practice. Or perhaps the decider is if such code is found inside Glibc or MUSL.
As this is a JIT and not an AOT compiler, you cannot optimize away unused flags. I expect the default behaviour is going to be not to set these flags on the assumption that modern software will not use them. If you just skip these flags, the generated RISC-V code stays fast.
You could have a command-line switch that enables this flag behaviour for software that needs it (with a big performance hit). This switch could take advantage of the RISC-V extension, if it exists, to speed things up.
Outside of this niche functionally though, it seems that the x86-64 instructions are mapping to RISC-V well. The extensions actually needed for good performance are things like the vector extension and binary bit manipulation.
Linux benefits from a different kind of integration. The article states that Apple is able to pull-off this optimization because they create both the translation software and the silicon. But the reason they need to worry about these obscure instructions is because the x86-64 software targeting macOS arrives as compiled binaries built using who knows what technology or toolchain. The application bundle model for macOS applications encourages ISVs to bundle in whatever crazy dependencies they like. This could include hand-rolled assembler you wrote decades ago. To achieve reliable results, Apple has to emulate these corner cases.
On Linux, the model is that everybody uses the same small subset of compilers to dynamically link against the same c runtimes and supporting libraries (things like OpenSSL or FreeTyoe). Even though we distribute Linux binaries, they are typically built from source that is portable across multiple architectures.
If GCC and Glibc or Clang and MUSL do not output certain instructions, a Linux x86-64 emulator can assume a happy path that does not bother emulating them either.
Ironically, a weakness in my assumptions here could be games. What happens when the x86-64 code we want to emulate is actually Windows code running on Linux. Now we are back to not knowing what crazy toolchain was used to generate the x86-64 and what instructions and behaviour it may depend on.
So, when I think “emulation” I usually consider it to be software emulating a hardware device (e.g. the original Gameboy, audio cards for legacy programmes that required audio cards, etc.). What they’re describing in the article is what has been described to me as being an abstraction/compatibility layer. So my questions are: 1.a. Is that really what this is or is it actually an emulator? b. If the latter, what makes it an emulator rather than a compat layer? 2. In general, how much do the two concepts interact? E.g. separate concepts entirely, ends of some continuum, etc.
When you implement the functionality of a piece of hardware in software, the software is said to “emulate” the hardware. The emulators you are used to are emulators, not because they are emulating a console (ex. N64), but because they’re emulating the hardware that was used to build that console (ex. a MIPS processor). That said, oftentimes console emulators need to account for specific quirks/bugs that were introduced specifically because of choices the console designers made. Ex. maybe the specific processor and memory they used for the N64 have a weird interaction that game devs at the time abused, and if your emulation doesn’t ensure that quirk works the same way, then some games won’t run.
At the risk of adding unnecessary detail, a VM might use emulation or it might not. The QEMU package is often used for virtualization, but despite its name (Quick Emulator) if the system you’re virtualizing matches the architecture of the system you’re running on, no emulation is needed.
\1a) In this case, it is risc-v hardware running software (built for risc-v) that emulates x86_64 hardware so that it can run an x86_64 binary.
\1b) A compatibility layer is less well defined, but in general refers to: whatever environment is needed to get a binary running that was originally built for a different environment. This usually includes a set of libraries that the binary is going to assume are loaded, as well as accounting for any other possible unique attributes in the structure of the executable. But if the binary, the compatibility layer, and the CPU are all x86_64, then there’s no emulation involved.
\2) to get a binary built for x86_64 windows running on risc-v Linux, you will need both emulation and a compatibility layer. In theory those two don’t need to be developed in tandem, or even know about each other at runtime, but i expect that there may be performance optimizations possible if they are aware if each other.
I mentioned QEMU because my first thought when reading this was, isn’t this a prime usecase for QEMU?
The hardware being emulated here is the CPU.
To me it sounds like what Java or .NET JIT does. I doubt it falls strictly into emulation 🤷♂️
Yes, JIT is used for both, but we don’t call JITing of Java/.Net bytecode “emulation” because there is no hardware that natively runs bytecode that we are emulating. Unlike x86_64 asm, bytecode is designed to be JITed. But yes, JITing is the defacto strategy for efficiently emulating one piece of hardware on another.
In Java or .NET, the JIT is still going from a higher level abstraction to a lower one. You JIT from CIL (common intermediate language) or Java Bytecode down to native machine code.
When you convert from a high level language to a low level language, we call it compiling.
Here, you are translating the native machine code of one architecture to the native machine code of another (x86-64 to RISC-V).
When you run code designed for one platform on another platform, we call it emulation.
JIT means Just-in-Time which just means it happens when you “execute” the code instead of Ahead-of-Time.
In .NET, you have a JIT compiler. Here, you have a JIT emulator.
A JIT is faster than an interpreter. Modern web browsers JIT JavaScript to make it faster.
Another awesome stopgap measure like Proton before the Linux and RISC-V standards take over the market!
This is a Linux x86-64 to Linux RISC-V emulator. It will not execute non-Linux code or execute code outside Linux.
The Linux system call interface is the same on both sides so, when it encounters a Linux system call in the x86-64 code, it makes that call directly into the RISC-V host kernel. It is only emulating the user space. This makes it faster.