Since they control the silicon, Apple might also be leveraging a specialized instruction / feature on the CPUs (e.g. a "foreign" bit that's able to mark memory pages as being from another architecture, and some addressing scheme that links them to one or more native pages behind the scenes)
Maybe the A series chips even have "extra" registers / program counters / interrupts that aid in accelerating this emulation process.
During the "do things to IR" phase of compilation, you can do static analysis, and use this to inform the rest of the compilation process.
The unique pattern of machine-code that must occur in any implementation of JIT, is a jump to a memory address that was computed entirely at runtime, i.e. with a https://en.wikipedia.org/wiki/Use-define_chain for that address value that leads back to a call to mmap(2) or malloc(2). Static analysis of the IR can find and label such instructions; and you can use this to replace them in the IR with more-abstract "enter JITed code" instrinsic ops.
Then, in the codegen phase of this compiler, you can have such intrinsics generate a shim of ARM instructions. Conveniently, since this intrinsic appears at the end of the JIT process, the memory at the address passed to the intrinsic will almost certainly contain finalized x86 code, ready to be re-compiled into ARM code. So the shim can just do a (hopefully memoized) call to the Rosetta JIT translator, passing the passed-in address, getting back the address of some ARM code, and jumping to that.
This is probably mostly true. But actually reliably finding such cases, in the most hostile environment imaginable (namely, on x86-64 binary code), would require heroic program analysis that I as a compiler engineer would judge unrealistic.
I would find it much more plausible for the system to watch for something like "jump to a target in a memory page that at some point had the W flag set, has since had W removed and X set, and has not been jumped to since that", and then trigger a compilation of that page starting at the given jump address, and a rewrite of the jump instruction. As proposed in https://news.ycombinator.com/item?id=23614894 this could be done by watching for page faults, though presumably it would mean intercepting places where the application tries to set the X bit on a page and setting NX instead so that the page fault can be caught.
LLVM is kind of slow; JavaScriptCore abandoned it years ago for their FTL backend.
> The unique pattern of machine-code that must occur in any implementation of JIT, is a jump to a memory address that was computed entirely at runtime, i.e. with a https://en.wikipedia.org/wiki/Use-define_chain for that address value that leads back to a call to mmap(2) or malloc(2).
void *region = mmap(0x100000000, 0x1000, PROT_READ | PROT_WRITE, MAP_FIXED, 0, 0);
memcpy(region, code, 0x1000);
mprotect(0x100000000, 0x1000, PROT_READ | PROT_EXEC);
reinterpret_cast<(void (*)())>(region)();Huh, this is news to me. The memcpy(3) man page on my Linux box (there is no memcpy(2) here) doesn't mention this either, is this some special MacOS or BSD feature of memcpy? Under what circumstances would it determine that it should fail?
Your reasoning is strange anyway since the memcpy has nothing to do with anything, the implicit information flow from mmap to mprotect would exist even if the memcpy and the region variable were removed:
mmap(0x100000000, 0x1000, PROT_READ | PROT_WRITE, MAP_FIXED, 0, 0);
mprotect(0x100000000, 0x1000, PROT_READ | PROT_EXEC);First, most programs do much of their work inside the OS - rendering, network, interaction, whatever, so that's not emulated, Rosetta just calls the native OS functions after doing whatever input translation is necessary. So, nothing below a certain set of API's is translated.
You have to keep a separate translated binary in memory, and be able to compile missing bits as you encounter them, while remembering all your offset adjustments. It worked amazingly well during the PowerPC transition. Due to so many things running natively on x86, the translated apps frequently faster than running native on PowerPC macs!
This gets repeated a lot and it's generally false. In fact, most of the time they were slower, and realistically you would expect this. On a clock for clock basis, an OG Mac Pro 2.66GHz was about 10-20% slower than a Quad G5 2.5GHz running the same PowerPC software. In some benchmarks, the Quad G5 was still faster at running PowerPC software than the 3.0GHz (see https://barefeats.com/quad06.html ). When the Core Duo had to pay the Rosetta tax, even upper-spec G4s could get past it (https://barefeats.com/rosetta.html) and it stood no chance against the G5.
Where I think this misconception comes from is that on native apps (and Universal apps have native code), these first Intel Macs cleaned the floor with the G4 and most of the time nudged past even the mighty Quad, and by the second generation it wasn't a contest anymore. But for the existing PowerPC software that was available during the early part of the Intel transition, Power Macs still ran PowerPC software best overall. It wasn't Rosetta that made the Intel Macs powerful, it was just bridging things long enough to buy time for Universal apps to emerge. Rosetta/QuickTransit was marvelous technology, but it wasn't that marvelous.
This means early on in the program's run you spend a lot of time translating code, but it pretty quickly stabilizes and you spend most of your time in already translated code.
Of course, if your program is self modifying then the system needs to do some more work to invalidate the translation cache when the underlying code is modified.
Here's a tutorial for a simple JIT that uses mmap() with PROT_EXEC to write machine code to before executing:
So what you'll do is silently enforce W^X to trap code modifications, but not propagate those traps to the emulated code.
I don't see anything here that requires extra kernel hooks.
What that practically means is that x86 executable pages get mapped as RO by default as seen by the ARM side even if they would normally be RWX. Modifications trap, the handler mprotects the pages as writeable, flush the translation cache for that region, then returns from the trap. Then on an x86 jump to that region, the JIT cache sees no traces for that region, marks the page as RO again so that modifications will trap, and starts recompiling (or maybe just interpreting; they may be using a tiered approach in the JIT).
This is some serious black magic, especially if it actually works. Even if it only works some of the time I give it an A for effort, since they could easily have just said "JITs will not work with Rosetta." Most applications do not contain JITs.
My guess is there are enough important Mac apps that contain JITs of some form to merit this work, probably high performance math and graphics kernels that JIT processing pipelines or something like that. I wonder if some pro studio editing applications do this.
Java apps and JavaScript apps (Electron) come to mind.
Some apps out there are between a rock and a hard place if it weren't for rosetta.
If there were ever a time and a place for black magic, it would be this use case: swapping out an enormous layer (the hardware) at the bottom of the stack, while keeping everything at the top of the stack working seamlessly.
https://www.reddit.com/r/emulation/comments/2xq5ar/how_close...
I'm more interested in how they're doing the AOT conversion and (presumably) patching the result to still be able to emulate for JITs. That'd be (comparatively) simple if it was just for things from the iOS and Mac App Stores since Apple has the IR for them still but they made it sound like it was more generic than that.
Don’t they need to communicate to developers and users what software will (and will not) run on their upcoming computers?
Because they're trying to get the message across to regular people (not HN types) that their software will continue working with the new chips, and they don't have to flee the Apple ecosystem.
That's par for course with Apple. They never acknowledge competitors including when its themselves. Everything they do or describe is awesome and magical, right now! It could be an incremental improvement, it could be a half decade old established technology, it could be something completely unexpected and science fiction turned into reality. Thats just how Apple works.
Unlike which company that does?
That's referencing them to tell how better you are: "Optional Clear View™ mast offers up to 40% greater visibility than leading competitors."
Apple does that too.
JITs have to explicitly make the memory they write the generated code to executable. The OS "just" needs to fail to actually make the page executable, and then handle the subsequent page fault by transpiling or interpreting the x86 code therein.
How on Earth does it do that? If executable code is being generated at runtime, it's going to be x86_64 binary machine code still (there are too many ways to generate valid machine code, and it won't know right away whether you're JITting, or cross compiling and actually want x86_64), so Rosetta would need to detect when the code's about to be run, or when it's marked as executable, and translate the machine code in that bit of memory just in time. The length of the ARM code might be longer, so it would have to be in a different block of memory, with the original x86_64 code replaced with a jump to the new code or something.
It's late at night here, so maybe I'm missing a simpler approach, but I'm a bit surprised they have it working reliably enough to make such a general statement (there being a great variety of JIT systems). From a quick search I can't tell if Microsoft's x86-on-ARM translation in Windows 10 ARM supports JITs in the program being run.