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

问题描述 投票:0回答:1

假设我有以下示例:

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

c++11 pointers x86-64 canonical-form
1个回答
2
投票

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

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

如果您希望此代码也适用于内核代码,您可能希望使用签名的x <<= 16; x >>= 16;使用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 (32-bit pointers in x86-64 long mode),你的代码可能仍然只是工作,并将uint64_t转换为指针类型将简单地丢弃高位。所以你对他们做了什么并不重要,首先零扩展将有希望优化。

所以你的uint64_t成员最终会将指针存储在低32位,而你的标记位位于高位32,但效率不高但仍然有效。也许在模板中检查sizeof(void*)以选择实现?


Future proofing

具有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内核中已经支持它,因此它已经准备好出现实际的硬件。

(我无法确定它是否会在冰湖中出现。)

有关48位虚拟地址限制的来源,请参阅Why in 64bit the virtual address are 4 bits short (48bit long) compared with the physical address (52 bit long)?


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

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

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

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


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

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

© www.soinside.com 2019 - 2024. All rights reserved.