高位是保留的,以防将来增加地址总线,所以你不能像那样简单地使用它
AMD64 架构定义了 64 位虚拟地址格式,其中低位 48 位在当前实现中使用(...)架构定义允许在未来的实现中将此限制提高到完整的 64 位,扩展了虚拟地址空间为 16 EB(2个 64字节)。相比之下,x86 只有 4 GB(2 32字节)。
http://en.wikipedia.org/wiki/X86-64#Architectural_features
更重要的是,根据同一篇文章[Emphasis mine]:
...在架构的第一个实现中,实际上只有虚拟地址的最低有效 48 位会用于地址转换(页表查找)。此外,任何虚拟地址的第 48 位到第 63 位都必须是第 47 位的副本(以类似于符号扩展的方式),否则处理器将引发异常。符合此规则的地址称为“规范形式”。
由于即使高位未被使用,CPU 也会检查它们,因此它们并不是真正“无关紧要”的。在使用指针之前,您需要确保地址是规范的。其他一些 64 位架构(如 ARM64)可以选择忽略高位,因此您可以更轻松地将数据存储在指针中。
也就是说,在 x86_64 中,如果需要,您仍然可以自由使用高 16 位(如果虚拟地址不超过 48 位,请参见下文),但您必须先通过符号扩展来检查和修复指针值取消引用。
请注意,将指针值转换long
为不是正确的方法,因为long
不能保证足够宽以存储指针。您需要使用uintptr_t
或intptr_t
。
int *p1 = &val; // original pointer
uint8_t data = ...;
const uintptr_t MASK = ~(1ULL << 48);
// === Store data into the pointer ===
// Note: To be on the safe side and future-proof (because future implementations
// can increase the number of significant bits in the pointer), we should
// store values from the most significant bits down to the lower ones
int *p2 = (int *)(((uintptr_t)p1 & MASK) | (data << 56));
// === Get the data stored in the pointer ===
data = (uintptr_t)p2 >> 56;
// === Deference the pointer ===
// Sign extend first to make the pointer canonical
// Note: Technically this is implementation defined. You may want a more
// standard-compliant way to sign-extend the value
intptr_t p3 = ((intptr_t)p2 << 16) >> 16;
val = *(int*)p3;
WebKit 的 JavaScriptCore 和 Mozilla 的 SpiderMonkey 引擎以及LuaJIT在nan-boxing 技术中使用了这个。如果值为 NaN,则低 48 位将存储指向对象的指针,高 16 位用作标记位,否则为双精度值。
以前Linux也使用GS基地址的第63位来表示该值是否被内核写入
实际上,您通常也可以使用第 48位。因为大多数现代 64 位操作系统将内核和用户空间分成两半,所以第 47 位始终为零,您有 17 个最高位可供使用
您还可以使用低位存储数据。它被称为标记指针。如果int
是 4 字节对齐,则 2 个低位始终为 0,您可以像在 32 位体系结构中一样使用它们。对于 64 位值,您可以使用 3 个低位,因为它们已经是 8 字节对齐的。同样,您还需要在取消引用之前清除这些位。
int *p1 = &val; // the pointer we want to store the value into
int tag = 1;
const uintptr_t MASK = ~0x03ULL;
// === Store the tag ===
int *p2 = (int *)(((uintptr_t)p1 & MASK) | tag);
// === Get the tag ===
tag = (uintptr_t)p2 & 0x03;
// === Get the referenced data ===
// Clear the 2 tag bits before using the pointer
intptr_t p3 = (uintptr_t)p2 & MASK;
val = *(int*)p3;
一个著名的用户是具有SMI(小整数)优化的 V8 引擎。地址中的最低位将用作类型的标记:
- 如果为 1,则该值是指向真实数据(对象、浮点数或更大的整数)的指针。下一个高位 (w) 指示指针是弱的还是强的。只需清除标签位并取消引用它
- 如果它是 0,它是一个小整数。在带有指针压缩的 32 位 V8 或 64 位 V8 中,它是一个 31 位 int,执行有符号右移 1 以恢复该值;在没有指针压缩的 64 位 V8 中,它是上半部分的 32 位 int
32-bit V8
|----- 32 bits -----|
Pointer: |_____address_____w1|
Smi: |___int31_value____0|
64-bit V8
|----- 32 bits -----|----- 32 bits -----|
Pointer: |________________address______________w1|
Smi: |____int32_value____|0000000000000000000|
https://v8.dev/blog/pointer-compression
如下所述,英特尔发布了PML5,它提供了一个57 位的虚拟地址空间,如果你在这样的系统上,你只能使用 7 个高位
不过,您仍然可以使用一些解决方法来获得更多免费位。首先,您可以尝试在 64 位操作系统中使用 32 位指针。在 Linux 中,如果允许 x32abi,则指针只有 32 位长。在 Windows 中,只需清除/LARGEADDRESSAWARE
标志,指针现在只有 32 位有效位,您可以将高 32 位用于您的目的。请参阅 如何在 Windows 上检测 X32?. 另一种方法是使用一些指针压缩技巧:V8 中的压缩指针实现与 JVM 的压缩 Oops 有何不同?
您可以通过请求操作系统仅在低区域分配内存来进一步获得更多位。例如,如果您可以确保您的应用程序永远不会使用超过 64MB 的内存,那么您只需要一个 26 位地址。如果所有分配都是 32 字节对齐的,那么您还有 5 位要使用,这意味着您可以在指针中存储 64 - 21 = 43 位信息!
我想ZGC就是一个例子。它仅使用 42 位进行寻址,允许 2 42字节 = 4 × 2 40字节 = 4 TB
因此,ZGC 仅保留从地址 4TB 开始的 16TB 地址空间(但实际上并未使用所有这些内存)。
ZGC 初探
它使用指针中的位,如下所示:
6 4 4 4 4 4 0
3 7 6 5 2 1 0
+-------------------+-+----+-----------------------------------------------+
|00000000 00000000 0|0|1111|11 11111111 11111111 11111111 11111111 11111111|
+-------------------+-+----+-----------------------------------------------+
| | | |
| | | * 41-0 Object Offset (42-bits, 4TB address space)
| | |
| | * 45-42 Metadata Bits (4-bits) 0001 = Marked0
| | 0010 = Marked1
| | 0100 = Remapped
| | 1000 = Finalizable
| |
| * 46-46 Unused (1-bit, always zero)
|
* 63-47 Fixed (17-bits, always zero)
有关如何执行此操作的更多信息,请参阅
旁注:与指针相比,在键值很小的情况下使用链表会浪费大量内存,而且由于缓存局部性不好,它也会变慢。事实上,你不应该在大多数现实生活中的问题中使用链表