【问题标题】:Pointers to static variables must respect canonical form?指向静态变量的指针必须尊重规范形式?
【发布时间】:2025-12-27 06:55:07
【问题描述】:

假设我有以下示例:

struct Dummy {
    uint64_t m{0llu};

    template < class T > static uint64_t UniqueID() noexcept {
        static const uint64_t uid = 0xBEA57;
        return reinterpret_cast< uint64_t >(&uid);
    }
    template < class T > static uint64_t BuildID() noexcept {
        static const uint64_t id = UniqueID< T >()
               // dummy bits for the sake of example (whole last byte is used)
               | (1llu << 60llu) | (1llu << 61llu) | (1llu << 63llu);
        return id;
    }
    // Copy bits 48 through 55 over to bits 56 through 63 to keep canonical form.
    uint64_t GetUID() const noexcept {
        return ((m & ~(0xFFllu << 56llu)) | ((m & (0xFFllu << 48llu)) << 8llu));
    }
    uint64_t GetPayload() const noexcept {
        return *reinterpret_cast< uint64_t * >(GetUID());
    }
};

template < class T > inline Dummy DummyID() noexcept {
    return Dummy{Dummy::BuildID< T >()};
}

非常清楚结果指针是程序中静态变量的地址。

当我调用GetUID() 时,我是否需要确保第 47 位重复到第 63 位?

或者我可以只使用低 48 位掩码进行 AND 并忽略此规则。

我无法找到有关此的任何信息。我假设这 16 位可能总是0

此示例严格限于 x86_64 架构 (x32)。

【问题讨论】:

  • 这段代码应该做什么?什么是“规范形式”?
  • AMD 64 位体系结构指针中的一条规则,其中第 48 位到 63 位必须是第 47 位的副本。更多信息请参阅en.wikipedia.org/wiki/X86-64#Virtual_address_space_details@ 64 位中只有 48 位被指针使用(现在)。因此,理论上您可以使用最后 16 位将其他信息与指针相关联。假设您在尝试访问该地址之前会丢弃该信息。
  • 至于代码应该做什么。在这个例子中什么都没有。但对于我的用例。它应该充当轻量级 RTTI 替代品。没有继承,没有复杂的东西。只是想成为一个类似于std::type_info 的结构,而不是0xBEA57,它将是一个指向包含有关类型(包括名称)的额外信息的结构的指针。未使用的位将用于缓存简单信息。比如类型是否为 POD,移动/复制可构造/可赋值等。
  • 具有用于57 位规范地址的 5 级页表的 CPU 可能很快就会出现。 Intel 已经发布了software.intel.com/sites/default/files/managed/2b/80/… / en.wikipedia.org/wiki/Intel_5-level_paging 并且已经在 Linux 内核中支持它,因此它已经为实际硬件的出现做好了准备。 (我不知道它是否会出现在冰湖中。)另见Why in 64bit the virtual address are 4 bits short (48bit long) compared with the physical address (52 bit long)?

标签: c++11 pointers x86-64 canonical-form


【解决方案1】:

在主流 x86-64 操作系统的用户空间代码中,您通常可以假设任何有效地址的高位为零。

AFAIK,所有主流 x86-64 操作系统都使用 high-half kernel 设计,其中用户空间地址始终处于较低的规范范围内。

如果您也希望此代码在内核代码中工作,您可能希望使用签名的int64_t xx &lt;&lt;= 16; x &gt;&gt;= 16; 进行签名扩展。


如果编译器不能将0x0000FFFFFFFFFFFF = (1ULL&lt;&lt;48)-1 保存在一个寄存器中以供多次使用,那么 2 班次可能会更有效。 (mov r64, imm64 创建宽常量是一条 10 字节指令,有时解码或从 uop 缓存中获取可能会很慢。)但如果您使用 -march=haswell 或更高版本进行编译,则 BMI1 可用,因此编译器可以做mov eax, 48/bzhi rsi, rdi, rax。但是,无论哪种方式,一个 AND 或 BZHI 仅是指针的 1 个关键路径延迟周期,而 2 个班次则为 2 个周期。不幸的是,BZHI 不能用于立即操作数。 (与 ARM 或 PowerPC 相比,x86 位域指令大多很糟糕。)

您当前提取位 [55:48] 并使用它们替换当前位 [63:56] 的方法可能更慢,因为编译器必须屏蔽旧的高字节,然后在新的高字节中进行 OR高字节。这已经是至少 2 个周期的延迟了,所以你最好只是换档,或者掩码可以更快。

x86 有废话位域指令,所以这从来都不是一个好计划。不幸的是,ISO C++ 不提供任何保证算术右移,但是在所有实际的 x86-64 编译器上,&gt;&gt; 在有符号整数上是一个 2 的补码算术移位。如果您想非常小心地避免 UB,请对无符号类型进行左移以避免有符号整数溢出。

int64_t 保证为 2 的补码类型,如果存在则没有填充。

我认为int64_t 实际上是比intptr_t 更好的选择,因为如果你有 32 位指针,例如Linux x32 ABI (32-bit pointers in x86-64 long mode),您的代码可能仍然正常工作,并且将 uint64_t 转换为指针类型只会丢弃高位。所以不管你对他们做了什么,零扩展首先会被优化掉。

因此,您的uint64_t 成员最终只会将指针存储在低 32 位中,而您的标签位则存储在高位 32 位中,效率有些低,但仍然有效。也许在模板中检查sizeof(void*) 以选择实现?


面向未来

具有用于57 位规范地址的 5 级页表的 x86-64 CPU 可能很快就会出现,以允许使用大内存映射的非易失性存储,如 Optane / 3DXPoint NVDIMM .

英特尔已经发布了 PML5 扩展提案https://software.intel.com/sites/default/files/managed/2b/80/5-level_paging_white_paper.pdf(请参阅https://en.wikipedia.org/wiki/Intel_5-level_paging 了解摘要)。 Linux 内核中已经支持它,因此它已经为实际硬件的出现做好了准备。

(我不知道它是否会出现在冰湖中。)

另请参阅Why in 64bit the virtual address are 4 bits short (48bit long) compared with the physical address (52 bit long)?,了解有关 48 位虚拟地址限制来自何处的更多信息。


因此您仍然可以将高 7 位用于标记指针并保持与 PML5 的兼容性。

如果您假设用户空间,那么您可以使用前 8 位并进行零扩展,因为您假设第 57 位(第 56 位)= 0。

重做低位的符号(或零)扩展已经是最佳选择,我们只是将其更改为不同的宽度,只会重新扩展我们干扰的位。而且我们干扰了足够少的高位,即使在启用 PML5 模式并使用宽虚拟地址的系统上,它也应该是未来的证明。

在具有 48 位虚拟地址的系统上,将第 57 位广播到高位 7 仍然有效,因为第 57 位 = 第 48 位。如果您不干扰这些低位,则不需要重新写的。


顺便说一句,您的 GetUID() 返回一个整数。不清楚为什么需要它来返回静态地址。

顺便说一句,返回&amp;uid(只是一个相对于 RIP 的 LEA)可能比加载 + 重新规范化您的 m 成员值更便宜。将static const uint64_t uid = 0xBEA57; 移动到静态成员变量,而不是在一个成员函数中。

【讨论】: