我可以分别访问__uint128_t中的两个 64 位寄存器吗?

Can I access the two 64-bit registers in __uint128_t separately?

提问人:Knm 提问时间:7/29/2023 最后编辑:Sep RolandKnm 更新时间:7/30/2023 访问量:167

问:

请考虑下面的代码。我们知道变量存储在 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 个寄存器,而不是执行和操作?我问这个问题是因为对于我的项目,这部分代码将被执行一万亿+次。所以最好先验证这个疑问。&>>

有什么汇编代码可以玩弄的吗?

C 程序集 CPU 寄存器 int128

评论

1赞 Fe2O3 7/29/2023
你有没有考虑过尝试一个?union
0赞 Toby Speight 7/29/2023
我认为,这个长常数会更清晰。~0UL
1赞 chux - Reinstate Monica 7/29/2023
@TobySpeight,并且不是明确的 64 位。最好在这里使用。unsigned long~0ULuint64_t
3赞 Toby Speight 7/30/2023
@chux,我当然完全同意。我只是按照问题的(可能被误导的)“要求”去做。为什么我们要在一个大小不确定的变量中存储 64 位,这对我来说是一个谜。

答:

10赞 Toby Speight 7/29/2023 #1

你写的东西可能是最好的,尽管通过强制转换截断比长常数更容易阅读。根据经验,如果你编写的代码清晰明了,那么你的编译器通常最容易看到你的意图并适当地进行优化。

编译器资源管理器中,我提供了以下函数:

#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

评论

1赞 Peter Cordes 7/29/2023
@TedLyngmo:不仅仅是 MSVC。任何面向 Windows x64 的主流编译器都必须使用 32 位才能与现有 DLL 兼容 ABI,除非它们提供备用标头,这对于每个第三方库来说都是不可能的。(虽然公平地说,显然 Windows GCC 的默认值是 10 字节,而 MSVC 是 8 字节,因此 GCC 与 MSVC 并不严格兼容。但是一种比 更广泛使用的类型,尤其是跨 ABI 边界(函数参数和结构))unsigned longlong doubleunsigned longlong double
1赞 Ted Lyngmo 7/29/2023
@PeterCordes我想在 C23 中,人们可以希望在 MSVC 中工作。Clang 已经支持它了unsigned _BitInt(128)
1赞 Peter Cordes 7/29/2023
@TedLyngmo:或者说,我本来打算在回答中提到的。_BitInt(128)
1赞 Eric Postpischil 7/29/2023
@PeterCordes:Re “C 保证从较宽的积分类型到较窄的模约简”:仅适用于无符号目标类型。对于已签名的目标类型,转换是实现定义的。
1赞 Toby Speight 7/30/2023
@chux这仅仅是因为我编译时没有任何通常的警告。对不起。
0赞 user21508463 7/29/2023 #2

变量不存储在寄存器中。它们存储在内存中并在寄存器中处理。

C 语言以多种方式提供映射数据的构造,例如union

union MyUnion
{
    __uint128_t a;
    unsigned long long b[2];
} u;

现在你可以随意引用,编译器被认为为给定的处理器生成了高效的代码。u.au.b[0]u.b[1]


请注意,使用掩码和移位的构造永远不会以这种方式实现,因为处理器无法一次性处理 128 个数据。相反,您的数字将始终作为两个 64 位数字进行处理。事实上,掩蔽和转移永远不会执行。a

评论

3赞 Peter Cordes 7/29/2023
变量不存储在寄存器中。在 C 抽象机器中为 true,但在某些情况下,“as-if”规则允许将变量完全优化到寄存器中,就像 Toby 和我的答案中的例子一样(其中函数参数在一对寄存器中传递,函数从不将其存储到内存中。这个问题被标记为 [assembly],所以很明显,他们担心编译器在一对寄存器中已经有值的情况。我希望他们意识到拆分不需要 asm 指令,问题只是如何将其表达给编译器。__int128
1赞 Peter Cordes 7/29/2023
我以为问题是关于优化的(“执行数万亿次......”),并试图避免混淆它们。
3赞 Peter Cordes 7/29/2023
有些数据根本不会存储在内存中。例如,函数的返回值(在 RDX:RAX 中)可以拆分为两半的单独 C 变量,而无需存储和重新加载。这花费零 asm 指令。(如果你把这些 C 变量分配给存储在内存中的东西,那么到底是原始__int128数据被存储了吗?在这里讨论语义没有帮助,重要的是查看您真正关心的循环的 asm。__int128
4赞 Toby Speight 7/29/2023
请注意,联合的可移植性不如问题中的代码,因为高低部分的顺序取决于处理器。
2赞 orlp 7/29/2023
请注意,虽然在 C 中随意访问成员是可以的,但从与 C++ 中存储的成员不同的成员读取是未定义的行为。
7赞 Peter Cordes 7/29/2023 #3

Shift/mask 或联合是要走的路。特别是如果您只想读取 的各个部分,位操作是清晰的,并且能够可靠地有效地编译。__int128

如果要替换上 64 位或下 64 位,联合可能会使编译器比按位掩码 / 移位 / OR 更容易看到它。如果这两种方式都能有效地编译,我不会感到惊讶,但 a 可能有利于人类的可读性。union

请注意,并集中各部分的顺序将取决于字节序,而位移则不然。


我建议使用 uint64_tunsigned long long 而不是 ,因为 Windows x64 使用 32 位 。大多数其他 64 位 ABI 使用 LP64 ABI,但 32 位的另一种情况是用于 64 位 CPU 的 ILP32 ABI,如 AArch64 ILP32 和 x32 ABI。 但仍受支持。unsigned longlonglongsizeof(void*) = 4__int128


我会使用强制转换将__int128截断为 64 位,而不必在 中键入正确数量的 s。对我来说,(uint64_t)a更符合托比的“明显和清晰”的准则。使强制转换明确,而不仅仅是通过分配给更窄的变量,这对人类读者来说是件好事。C 保证从较宽的积分类型到较窄的无符号类型的模减少,这意味着从无符号或 2 的补码有符号的源类型进行按位截断。(GCC 中的带符号整数始终是 2 的补码f0xffffffffffffffff

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__int128unsigned __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)

评论

0赞 Peter Cordes 7/30/2023
@Knm:你有没有遗漏一个运算符,比如vs.?我喜欢在二进制运算中将转换放在第二个操作数之前,以提醒读者它(在抽象机器中)它适用于二进制运算符之前,因此另一方也会得到提升,而不是截断结果。(强制转换的优先级高于 so,但另一种方式可以节省读者一些脑力来验证这一点。a*ba * (__uint128_t)*+(__uint128_t)a * b