4 ms·
The historical longevity of hard-wired pointer tagged systems isn't great. Others have alluded to early m68k Macintoshes ignoring the top 8 bits of pointers, r
by KMag 5y ago
The historical longevity of hard-wired pointer tagged systems isn't great.
Others have alluded to early m68k Macintoshes ignoring the top 8 bits of pointers, resulting in later Macintosh systems needing per-application compatibility mode to allow some applications to use more than 16 MB of RAM and others to keep using pointer tagging.
Sun's SPARC processors have dedicated instructions that have 30-bit addressing and 2 tag bits. It turned out basically no software ended up using those instructions, even Sun's JVM JITs on SPARC avoided these instructions, I believe.
There's older Lisp machines, Buroughs machines, etc. IBM's AS/400 Technology Independent Machine Interface (TIMI) (AoT compiled bytecode for userland executables, essentially) uses tagged 128-bit pointers. These all seemed better designed for longevity, but I'm less familiar with these details, and they're all significantly less popular than m68k or SPARC.
It's a shame that none of these pointer tagging solutions are implemented as faster/more compact encodings of common userspace pointer tagging operations already emitted by JITs. For instance, pointer tagging usually is used to inline the fast path where the tagged type is what's expected, with a conditional branch for handling all of the unexpected cases. So, a compact representation of "IP-relative jump by W if the X most significant bits of Y aren't Z" would reduce the code size (and instruction cache/memory bandwidth used) for dynamically typed language JITs. Similarly, it's common to have instructions for bitwise-and with a sign-extended small immediate (32 bits, 13 bits, etc., depending on architecture). For these pointer tagging operations, it's much more useful to instead use the immediate to specify the most significant N bits of the bitwise-and, with all of the low bits 1.
By not hard-wiring the number of bits used in tagging, the features would seem to be more future-proof. By making the instructions purely user-space, they wouldn't require any kernel support and could be adopted faster. Adoption in JITs (the most common current use case for tagged pointers) could be especially fast, since JITs can perform a runtime check to avoid emitting the new instructions on unsupported CPUs. Granted, the extent of my CPU design experience was one undergrad class. I'm sure it's much more complex to push this extra logic into the instruction decoding and micro-op issuing logic instead of modifying the MMU. However, hard-wired pointer tagging systems have typically had shorter lifetimes than their designers expected.
On a related note, if your userspace programs are at the top of your address space, you get NaN-boxed pointers "for free", since any 64-bit pointer with the first 13 bits set is some form of NaN when interpreted as an IEEE-754 double. What this means is that the different NaN-tagging tradeoffs (Safari decided to make object pointer use faster, Firefox chose to make Number use faster) collapse. Neither Numbers nor pointers need to be munged (though, needing to be careful about the two actual NaN values the FPU might generate). Unfortunately for Linux, too much kernel code (and perhaps also too much userland code) at this point assumes the kernel lives at the top of the address space for Linux to change this.
On a side note, garbage collectors would benefit with a feature allowing traps of writes to read-only memory to a userland handler without a context switch. That way, the collector can mark a few pages read-only, lazily copy objects and fix up pointers as user code mutates objects, and finally fix up any stragglers when the GC attempts to mark the unmarked objects during a mark phase. Some garbage collectors already do this sort of thing, but using SIGSEG handlers that trap to the kernel and call back. It would be nice to reduce the overhead of these. Presumably, installing these lightweight handlers would require kernel cooperation.
Historical tangent: early 16-bit x86 processors generated 20-bit addresses by left-shifting the segment by 4 bits, adding the address, and ignoring overflow. So, once machines started supporting more than 1 MB of memory (not ignoring the overflow), IBM needed a backward-compatibility mechanism. They had an unused pin on their keyboard controller chip, so they routed the 21st bit (address line 20, using zero-based numbering aka "a20") through the keyboard controller. The OS had to explicitly tell the keyboard controller to stop zeroing-out the "a20" line. So, during your boot up process, your x86 CPU still has to emulate the portion of the IBM keyboard controller that zeroes out the a20 line until the mode-switch. Your 64-bit machine boots with 20-bit addressing, switches to 21-bit addressing, switches to 32-bit addressing, and finally switches to 64-bit addressing. Given the BIOS checks that the first two bytes of the boot sector are 0x55AA, it's a shame that when designing x86_64, AMD didn't have the processor start in 64-bit addressing with an instruction to drop back down to 20-bit addressing for legacy systems. That way, the BIOS could use the first two bytes of the boot sector to determine if it needed to drop into legacy mode immediately before executing the boot sector. It's not a lot of transistors or complexity to emulate the accumulated cruft, but there was an opportunity to set a roadmap to remove that cruft from the hardware.