Top byte ignore for fun and memory savings
linaro.org
linaro.org
Lwn wrote about the linux support here, https://lwn.net/Articles/902094/
How many years till "256TB ought to be enough for anyone", will become a repeated quote? And we will wish we had kept those extra bytes available for addressing.
But putting metadata in the MSBs of pointers is a nice trick for concurrent garbage collection like Pauseless, ZGC, C4 and such [2]...though I'm not sure how much you win, as collections aren't _that_ frequent, so if you map the same physical memory multiple times (like ZGC, probably C4 too) your TLB should pick up whichever replica in virtual memory you're currently accessing quickly.
[0] https://gitlab.com/Linaro/tcwg/tbi_lisp/-/blob/main/SymbolTa...
[1] https://www.cs.princeton.edu/research/techreps/TR-412-93
[2] https://www.usenix.org/legacy/events/vee05/full_papers/p46-c... - the Azul hardware seemingly only stole one bit still.
I recently converted a Lisp to NaN tagging. All it needs is that pointers are restricted to the 48 bit range, not that the top 16 bits are ignored.
All values which are pointers have a zero there; if there are nonzero bits, the value is something other than a pointer, and not equal to any pointer.
Other choices are possible within the scope of NaN tagging: it's possible to favor numbers so that values which are floating-point numbers do not require any treatment before use, but recovering pointers requires bit operations.
Either way, the run-time never relies on two different values both being pointers to the same object, let alone both being dereferenced to access that object.
That could turn out to be inconvenient in some ways. While memory access is cheap due to not having to mask the special bits (the hardware doing that for "free"), all comparisons which check "are these two values the same object" are going to require masking, because your comparison instructions (xor, subtract, compare, ...) will not ignore any of the bits. Those word comparisons are pretty frequent operations in Lisp: e.g. whether two values are the same symbol. You more frequently compare a symbol than you peek into it to geet its properties!
(On one platform, the top bits are not ignored: Android. Its libc uses tagging for added safety. E.g. malloc hands you a tagged pointer. Only the low 48 bits of it matter for addressing, but the tag has to match when you call free. This causes a problem, since I need those bits to be zero in order for a value to be of pointer type. Luckily, I allocate heaps in large blocks. On Android, each block remembers its tag, and then I clear the pointer's top 16 bits. If/when the garbage collector is able to return an entire heap block to the system, those top bits are restored in the pointer handed to free, so everything is cool. The overhead of this is absolutely negligible, therefore. One word of storage per tens of thousands of objects, and some rarely executed bit operations.)
Wouldn’t two pointers to the same object also have the same tag? I don’t understand why you would need to mask it.
Instead of using true 64-bit addreses AArch64 systems only use 48-bits. In a decade or two (or maybe even less time) it's reasonable to expect systems will exist with > 256TB of memory in a single machine.
Thus is it ill-advised to leverage such a hackey feature? Particularly since the bits are guaranteed to rot.
From a power efficiency perspective the appeal of using only using the minimal set of 6/8 "strictly required" bytes is understandable.. Oh wait, this design choice doesn't reduce power consumption? Hrm.
Would appreciate hearing from an expert about why this is a thing. Also, if the addresses are virtualized, why not take the memory savings that'd come with efficiently packing the addresses into only 48-bytes? Too much overhead?
In general it's not a big issue. You only want to use it with proper CPU support anyways. It's just best to avoid using it for user code and leave it for use by the compiler and/or operating system.
For example, AArch64 supports Pointer Authentication[0]. This uses the additional bits to essentially "sign" a pointer, which could serve as a final defense against some attacks. It is essentially free, so why not use it for the dozen or so years those bits aren't needed yet?
[0]: https://www.qualcomm.com/content/dam/qcomm-martech/dm-assets...
That is the reason why almost every future architectures after the 68000 specified the behavior for such unused bits (in general it is to sign extend them, IOW, the top bits must be all 1s or all 0s). If that had been the case on the 68000 then when software attempted to access pointers with data in the high bits it would have caused an exception when the load / store was issued. It is worth noting it would not have prevented people from using the top bits, it would have just forced them to manually mask them in software (a technique that is used all over the place for things like tagged pointers, etc). That still might have been an issue when the memory space was actually expanded to the point where the high bits were needed for real physical or virtual addresses.
TBI is actually about explicitly stating architecturally that these 8 bits are used for data not addresses. That is different from the situation on 68000 because it is an architectural guarantee that future processors will not use those bits for addresses, which means you won't have bincompat issues and you don't have to bother masking them before use dereference them.
It's standard to store a byte or half-word in a struct with a full 32-bit or 64-bit word, for performance reasons.
Some RISC machines can't even do such sub-word accesses at all. Byte and half-word load/store are an optional extension to the RISC-V base instruction set.
But yeah, the idea is that you can add tagging info to the high bits of pointers (to enforce data types, reference counting, etc.) and pay zero cost to indirect through such a tagged pointer. No need to mask off the tag bits, just use it like a regular pointer.
Not an expert at all, but the the place where I've really used this sort of method is when writing a GC and trying to figure out where to store a "pointer has been visited" marker in the pointer instead of in a different data structure.
This was also on ARM, but quite a long time ago on a Sharp Zaurus. The word aligned pointer model gave 2 bits at the end of the pointer to mark cycles in GC pathway.
This was much simpler than a separate data structure to keep the mark phase of the traversal in memory & was an extension of the "1 bit reference counter" paper[1] with white, gray and black for the bits in there.
Of course, you need to go back and rewrite them out to 00, but with virtual memory in the mix you can play bigger tricks. This does save a lot of memory, hash lookups and is very low-overhead way of keeping a 1:1 metadata storage for every pointer you have.
You'll see the same trick in the JVM ZGC where higher bits are used to keep track of the metadata about the items being collected [2].
With a little bit of pointer arithmetic, you can also turn a pointer read-only to prevent some process from modifying shared data outside of a lock too, to avoid copying data before unlocking. I had a double mmap mode in PHP APC which segv'd when the core engine mutated a cached data structure outside of a lock[3].
[1] - https://link.springer.com/article/10.1007/BF01932156
[2] - http://hg.openjdk.java.net/zgc/zgc/file/59c07aef65ac/src/hot...
Intel processors support up to 57 bit virtual addresses (128 PB), in a 5-level page table. The trade-off to increased size is an increase in worst case latency reading a virtual address.
Note that this is a limit for address space, not for physical memory. You can need that address space e.g. for mmaping of large files. And disk arrays with files larger than 256 TB are realistic even today (that is just 16x 16TB HDD).
In Vale, we're planning on storing a small "offset-to-generation" integer in the top bits. This will let us easily find the top of the allocation that contains the object we're pointing to. When the user doesn't want to use the opt-in region borrow checker, this mechanism helps provide an easy fallback.
The technique could theoretically also be used for RC'd languages to allow taking references to the interior of objects, which (I believe) would help those languages close the gap between them and high-performance languages like Rust, Zig, Vale, etc.
I hope it catches on with more CPUs.
Another reason I'm excited for memory tagging is that it can somewhat reliably help identify memory unsafety in development/testing. Perhaps a low-level language (Zig? Odin?) could even make it work correctly in the presence of tagless unions, to enable a more efficient alternative to tagged unions (like in Rust) for certain situations.
> The technique could theoretically also be used for RC'd languages to allow taking references to the interior of objects, which (I believe) would help those languages close the gap between them and high-performance languages like Rust, Zig, Vale
I don’t understand how it would make any difference. You would still need update the reference count, which is what makes rc expensive. It would make accessing slightly faster, because you don’t need to mask the top bits but that wouldn’t make up for the bigger cost.
See https://apple.fandom.com/wiki/24-bit_addressing for more details.
Had the move to the m68020 and m68030 included mandatory memory management, then old tasks could've all been run in the first 16 megs and clean apps run anywhere else. It's a shame Apple didn't do this.
Since this is now a documented feature of the CPU, processes can have up to 65,536 terabytes of usable memory and still use this. If they need more than 65,536 terabytes of memory, they can simply not use this feature :)
Of course, on 68k Macs also the OS parts stored in ROM were not 32 bit "clean". MODE32, a tool developed by Connectix, patched the ROMs (traditional MacOS stores entry points for system functions in RAM which can be modified) to get rid of 24 bit addressing (https://en.wikipedia.org/wiki/MODE32).
What would be nice, of course, is to use the ARMv8-a top byte ignore feature to implement tagged data types for Lisp and Smalltalk implementations... [and I should read the whole article since this is a large part of it... oops]
"65,536 terabytes ought to be enough for anybody." :)
It would not have been feasible; RAM was expensive back then. Most of Apple's 68020 and 68030 machines only shipped with 1MB of RAM; it was not until 1992 that they started shipping machines with 4MB.
Obviously you don't need to use MODE32 if you don't use more than 8 megs. Same thing. Memory wouldn't be rearranged to have (stuff in the first 16 megs) and (everything else) until you have more memory than would fit in the first 16 megs.
The developers responsible for this 'abuse' were first and foremost Apple themselves - key services provided by the OS/ROMs used the high order bits for various things.
The pointer gets copied; the object does not. You're going to end up with copies of the pointer which have different reference counts, not knowing which one is the real one.
As a rule of thumb, no mutable property of the heap object should be forwarded into the pointer word used to reference it.
Not sure where the author is going with all that.
Edit: it may work if these pointers are handles in a table, which seems to be what the article is getting at. The actual value representation points or indexes into the table and those are passed around.
So the table is just a very compact way to store objects in a single pointer-sized word.
TBI is used by memory tagging extension (MTE; primary ingredient to "HWASAN"). I'd be pretty surprised if Apple Silicon didn't support it officially. IIRC they're 8.6 and MTE is specified in 8.5? Note that it's not the same as pointer authentication which Apple actively promotes and as I recall contributed on the community toolchain development for this feature.
The introduced incompatibility was that bad and hard to get rid of.
So pardon me for feeling uneasy.
A quick search hasn't yielded any info on when AMD support for this may be coming, but I'd assume it (or something similar) is in the works.
I would expect Intel or AMD to follow (if they haven't already).
What’s the practical purpose of implementing that in Ice Lake?