指向静态变量的指针必须遵守规范形式?
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;
移动到静态成员变量而不是在一个成员函数中。
假设我有以下示例:
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 地址始终处于较低的规范范围内。
如果您希望此代码也能在内核代码中运行,您可能希望使用带符号的 如果编译器不能将 您当前提取位 x86 有垃圾位域指令,所以这从来都不是一个好计划。不幸的是,ISO C++ 不提供任何 保证 算术右移,但是 在所有实际的 x86-64 编译器上, 我认为 所以您的 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 内核中已经支持它,因此它已为实际硬件的出现做好准备。 (不知道是不是在冰湖里) 另请参阅 因此您仍然可以将高 7 位用于标记指针并保持与 PML5 的兼容性。 如果您假设用户-space,那么您可以使用前 8 位和零扩展,因为您假设第 57 位(位 56)= 0。 重做低位的符号(或零)扩展已经是最佳选择,我们只是将其更改为不同的宽度,仅重新扩展我们干扰的位.而且我们正在扰乱一些足够高的位,即使在启用 PML5 模式和使用宽虚拟地址的系统上,它也应该是未来的证明。 在具有 48 位虚拟地址的系统上,向高 7 位广播第 57 位仍然有效,因为第 57 位 = 第 48 位。如果你不打扰那些低位,则不需要它们重写。 顺便说一句,您的 顺便说一句,return 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 个周期的延迟,因此您不妨直接移动或掩码,这样可以更快。>>
在有符号整数上是 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 实现?
面向未来
GetUID()
return 是一个整数。目前尚不清楚为什么需要 return 静态地址。&uid
(只是一个 RIP 相关的 LEA)可能比加载 + 重新规范化您的 m
成员值更便宜。将 static const uint64_t uid = 0xBEA57;
移动到静态成员变量而不是在一个成员函数中。