0

假设我有以下示例:

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)。

4

1 回答 1

2

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

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

如果您希望此代码也可以在内核代码中工作,您可能需要x <<= 16; x >>= 16;使用 signed进行符号扩展int64_t x


如果编译器不能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]的方法可能较慢,因为编译器必须屏蔽旧的高字节,然后在新的高字节中进行 OR。这已经是至少 2 个周期的延迟,所以您不妨换挡,或者掩码,这样可以更快。

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

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

我认为int64_t实际上是比 更好的选择intptr_t,因为如果您有 32 位指针,例如Linux x32 ABI(x86-64 长模式下的 32 位指针),您的代码可能仍然只是工作,并将 auint64_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 内核中已经支持它,因此它已经为实际硬件的出现做好了准备。

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

另请参阅为什么在 64 位中虚拟地址比物理地址(52 位长)短 4 位(48 位长)?有关 48 位虚拟地址限制来自何处的更多信息。


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

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

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

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


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

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

于 2019-04-29T22:40:52.190 回答