指向静态变量的指针必须遵守规范形式?

Pointers to static variables must respect canonical form?

假设我有以下示例:

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 位掩码与并忽略这条规则。

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

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

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

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

如果您希望此代码也能在内核代码中运行,您可能希望使用带符号的 int64_t x.[ 对 x <<= 16; x >>= 16; 进行符号扩展=35=]


如果编译器不能将 0x0000FFFFFFFFFFFF = (1ULL<<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] 的方法可能较慢 因为编译器必须屏蔽掉旧的高字节然后在新的高字节中进行或运算。这已经是至少 2 个周期的延迟,因此您不妨直接移动或掩码,这样可以更快。

x86 有垃圾位域指令,所以这从来都不是一个好计划。不幸的是,ISO C++ 不提供任何 保证 算术右移,但是 在所有实际的 x86-64 编译器上,>> 在有符号整数上是 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*) 以 select 实现?


面向未来

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

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

(不知道是不是在冰湖里)

另请参阅 以了解有关 48 位虚拟地址限制的来源的更多信息。


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

如果您假设用户-space,那么您可以使用前 8 位和零扩展,因为您假设第 57 位(位 56)= 0。

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

在具有 48 位虚拟地址的系统上,向高 7 位广播第 57 位仍然有效,因为第 57 位 = 第 48 位。如果你不打扰那些低位,则不需要它们重写。


顺便说一句,您的 GetUID() return 是一个整数。目前尚不清楚为什么需要 return 静态地址。

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