提问人:Knm 提问时间:7/29/2023 最后编辑:Sep RolandKnm 更新时间:7/30/2023 访问量:167
我可以分别访问__uint128_t中的两个 64 位寄存器吗?
Can I access the two 64-bit registers in __uint128_t separately?
问:
请考虑下面的代码。我们知道变量存储在 2 个 64 位寄存器中(假设是 x64 处理器)。要求是将前 64 位存储在一个无符号长变量中,将接下来的 64 位存储在另一个无符号长变量中。__uint128_t
__uint128_t a = SOMEVALUE;
unsigned long b = a&0xffffffffffffffff;
unsigned long c = a>>64;
在这里,b 存储前 64 位,c 存储接下来的 64 位。有没有其他更简单的方法可以分别访问 2 个寄存器,而不是执行和操作?我问这个问题是因为对于我的项目,这部分代码将被执行一万亿+次。所以最好先验证这个疑问。&
>>
有什么汇编代码可以玩弄的吗?
答:
你写的东西可能是最好的,尽管通过强制转换截断比长常数更容易阅读。根据经验,如果你编写的代码清晰明了,那么你的编译器通常最容易看到你的意图并适当地进行优化。
在编译器资源管理器中,我提供了以下函数:
#include <stdint.h>
void decompose(__uint128_t num, uint64_t *a, uint64_t *b) {
*a = (uint64_t)(num >> 64);
*b = (uint64_t)num;
}
当使用 编译为 x64 时,它会生成您想要的代码:gcc -O3
decompose:
mov QWORD PTR [rdx], rsi
mov QWORD PTR [rcx], rdi
ret
评论
unsigned long
long double
unsigned long
long double
_BitInt(128)
变量不存储在寄存器中。它们存储在内存中并在寄存器中处理。
C 语言以多种方式提供映射数据的构造,例如union
union MyUnion
{
__uint128_t a;
unsigned long long b[2];
} u;
现在你可以随意引用,编译器被认为为给定的处理器生成了高效的代码。u.a
u.b[0]
u.b[1]
请注意,使用掩码和移位的构造永远不会以这种方式实现,因为处理器无法一次性处理 128 个数据。相反,您的数字将始终作为两个 64 位数字进行处理。事实上,掩蔽和转移永远不会执行。a
评论
__int128
__int128
Shift/mask 或联合是要走的路。特别是如果您只想读取 的各个部分,位操作是清晰的,并且能够可靠地有效地编译。__int128
如果要替换上 64 位或下 64 位,联合
可能会使编译器比按位掩码 / 移位 / OR 更容易看到它。如果这两种方式都能有效地编译,我不会感到惊讶,但 a 可能有利于人类的可读性。union
请注意,并集中各部分的顺序将取决于字节序,而位移则不然。
我建议使用 uint64_t
或 unsigned long long
而不是 ,因为 Windows x64 使用 32 位 。大多数其他 64 位 ABI 使用 LP64 ABI,但 32 位的另一种情况是用于 64 位 CPU 的 ILP32 ABI,如 AArch64 ILP32 和 x32 ABI。 但仍受支持。unsigned long
long
long
sizeof(void*) = 4
__int128
我会使用强制转换将__int128
截断为 64 位,而不必在 中键入正确数量的 s。对我来说,(uint64_t)a
更符合托比的“明显和清晰”的准则。使强制转换明确,而不仅仅是通过分配给更窄的变量,这对人类读者来说是件好事。C 保证从较宽的积分类型到较窄的无符号类型的模减少,这意味着从无符号或 2 的补码有符号的源类型进行按位截断。(GCC 中的带符号整数始终是 2 的补码。f
0xffffffffffffffff
a>>64
完全没问题。即使对于有符号,算术右移然后分配给 64 位类型也会丢弃高 64 符号位,这些符号位可能是全 1 或全零,GCC 仍将对其进行优化。__int128
#include <stdint.h>
uint64_t foo_signed (__int128 num) {
return (num >> 64) + (uint64_t)num;
// Intentionally sloppy in the abstract machine to see what happens:
// (u64)num is promoted back to 128-bit for + (with zero-extension because it's unsigned)
// then the + result truncated to uint64_t for return.
// GCC still avoids actually generating the high half of the signed shift result.
}
uint64_t foo_unsigned (unsigned __int128 num) {
return (num >> 64) + (uint64_t)num;
}
这两者都编译为 x86-64。(戈德博尔特)。lea rax, [rdi + rsi]
ret
128 位整数的类型名称
在现代 GNU C 中,手册目前只提到 () ,而没有提到 .unsigned
__int128
__uint128_t
AFAIK,继续使用遗留并没有错;GCC 开发人员没有理由想要删除相同类型的名称。参见 gcc 中是否有 128 位整数?- 自 GCC4.6 以来一直存在,在这一点上已经很老了。但是,除非您关心古老的 GCC 版本,否则我建议您使用新代码,就像上面的示例一样。__uint128_t
__int128
unsigned __int128
在 ISO C23 中,将被标准化,因此您可能更喜欢它。但最后我检查了一下,只有 clang 支持它(但不限于 64 位目标的方式)。unsigned _BitInt(128)
__int128
__uint128_t
在新代码中,最好使用 typedef
这使您可以根据需要更改为便携式,并节省键入时间。_BitInt
#ifdef defined(__SIZEOF_INT128__)
typedef unsigned __int128 u128;
// or __uint128_t for compat with even older GCC which doesn't define __SIZEOF_INT128__
#elif ??? // feature-test macro for this C23 feature?
typedef unsigned _BitInt(128) u128;
#else
#error no 128-bit integer type available
#endif
// then use u128 in later code.
如果您发现移位和/或转换会给代码增加噪音,则可以编写辅助函数或宏。
static inline uint64_t hi64(u128 a) { return a >> 64; }
static inline uint64_t lo64(u128 a) { return (uint64_t)a; }
然后,您可以简单地使用 和/或 .hi64(x)
lo64(x)
评论
a*b
a * (__uint128_t)
*
+
(__uint128_t)a * b
评论
union
~0UL
unsigned long
~0UL
uint64_t