C语言 我可以单独访问__uint128_t中的两个64位寄存器吗?

ryoqjall  于 2023-08-03  发布在  其他
关注(0)|答案(3)|浏览(123)

考虑下面的代码。我们知道__uint128_t变量存储在2个64位寄存器中(假设x64处理器)。要求是将前64位存储在一个无符号长变量中,将后64位存储在另一个无符号长变量中。

__uint128_t a = SOMEVALUE;
unsigned long b = a&0xffffffffffffffff;
unsigned long c = a>>64;

字符串
这里,B 存储前64位,c 存储后64位。是否有其他更简单的方法来分别访问2个寄存器,而不是执行&>>操作?我问这个是因为对于我的项目来说,这段代码将被执行一万多亿次。所以还是先验证一下这个疑惑吧。
有什么我能用的汇编代码吗?

nuypyhwy

nuypyhwy1#

您所写的可能是最好的,尽管通过强制转换进行截断比长常量更容易阅读。作为一个经验法则,如果你写的代码是明显和清晰的,那么编译器通常最容易看到你的意图并进行适当的优化。
Compiler Explorer上,我提供了这个函数:

#include <stdint.h>

void decompose(__uint128_t num, uint64_t *a, uint64_t *b) {
    *a = (uint64_t)(num >> 64);
    *b = (uint64_t)num;
}

字符串
当使用gcc -O3为x64编译时,它会生成您想要的代码:

decompose:
        mov     QWORD PTR [rdx], rsi
        mov     QWORD PTR [rcx], rdi
        ret

7qhs6swi

7qhs6swi2#

Shift/mask或工会是要走的路。特别是如果你只想读取__int128的部分,位操作是清晰的,并且可以可靠地高效编译。

**如果你要替换高64位或低64位,union**可能比按位掩码/移位/ OR更容易让编译器看到。如果这两种方式都能高效地编译,我不会感到惊讶,但union可能对人类的可读性有好处。

请注意,联合中的半部分的排序将取决于字节序,而位移位不依赖于字节序。
我建议使用**uint64_tunsigned long long**而不是unsigned long,因为Windows x64使用32位long。大多数其他64位ABI使用LP 64 ABI,但32位long的另一种情况是用于64位CPU的ILP 32 ABI,如AArch 64 ILP 32和x32 ABI。sizeof(void*) = 4,但仍支持__int128

我会使用强制转换将__int128截断为64位,而不必在0xffffffffffffffff中键入正确的f数量。对我来说,**(uint64_t)a**更好地遵循了Toby的“显而易见”的指导方针。使强制转换显式化,而不是仅仅通过赋值给一个更窄的变量,这对人类读者有好处。C保证从较宽的整数类型到较窄的无符号类型的模归约,这意味着从无符号或2的补码有符号的源类型进行按位截断。(GCC中的有符号整数是always 2's complement
a>>64完全没问题。即使对于有符号的__int128,算术右移然后赋值为64位类型也会丢弃高64个符号位,这些符号位可能是全1或全0,GCC仍然会优化它。

#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。(Godbolt)。

128位整数类型名

在现代GNU C中,the manual目前只提到(unsigned__int128,而不是__uint128_t
AFAIK,继续使用遗留__uint128_t没有错; GCC开发人员没有理由要删除相同类型的名称。请参阅Is there a 128 bit integer in gcc?-__int128自GCC4.6以来就一直存在,目前已经很老了。但除非你关心古老的GCC版本,否则我推荐unsigned __int128用于新代码,就像我上面的例子一样。
在ISO C23中,unsigned _BitInt(128)将被标准化,因此您可能更喜欢这样。但据我所知,只有clang支持它(但不限于像__int128/__uint128_t那样的64位目标)。

在新代码中,最好使用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)

m3eecexj

m3eecexj3#

变量不存储在寄存器中。它们存储在内存中,并在寄存器中进行处理。
C语言提供了union结构来以多种方式Map数据,如

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

字符串
现在你可以随意引用u.au.b[0]u.b[1],编译器被认为可以为给定的处理器生成高效的代码。
请注意,使用掩码和移位的构造永远不会这样实现,因为处理器无法一次处理128个数据。相反,您的a将始终被处理为两个64位数字。事实上,将永远不会执行掩蔽和移位。

相关问题