使用 uint64_t 的不便之处

Inconveniences of using uint64_t

我有一个高度可移植的库(它在任何地方都可以编译和运行良好,即使没有内核),我希望它尽可能保持可移植性。到目前为止,我已经避免使用 64 位数据类型,但我现在可能需要使用它们——准确地说,我需要一个 64 位位掩码。

我从来没有真正考虑过它,我也不是一个足够的硬件专家(尤其是在嵌入式系统方面),但我现在想知道:使用 uint64_t(或者,等价地,uint_least64_t)?我可以想到两种方法来解决我的问题:

  1. 实际可移植性:所有微控制器——包括 8 位CPU——都能够处理 64 位整数吗?
  2. 性能:与 32 位整数相比,8 位 CPU 对 64 位整数执行按位运算的速度有多慢?我正在设计的函数只有一个 64 位变量,但会对其执行 很多 位运算(即在循环中)。

对符合标准的 C 编译器有各种最低要求。 C 语言允许两种形式的编译器:hostedfreestanding。托管意味着 运行 在 OS 之上,独立 运行 没有 OS。大多数嵌入式系统编译器都是独立的实现。

独立编译器有一些回旋余地,它们不需要支持所有的标准库,但它们需要支持它们的最小子集。这包括 stdint.h(参见 C17 4/6)。这又要求编译器实现以下 (C17 7.20.1.2/3):

The following types are required:

int_least8_t int_least16_t int_least32_t int_least64_t
uint_least8_t uint_least16_t uint_least32_t uint_least64_t

所以微控制器编译器不需要支持uint64_t,但它必须(奇怪的是)支持uint_least64_t。实际上,这意味着编译器也可以添加 uint64_t 支持,因为在这种情况下是一样的。

至于8位MCU支持什么...它通过指令集支持8位运算,在某些特殊情况下还可以使用变址寄存器进行一些16位运算。但一般来说,只要使用大于 8 位的类型,就必须依赖软件库。

因此,如果您尝试在 8 bit 上进行 32 位运算,它会将一些编译器软件库与代码内联,结果将是数百条汇编指令,使此类代码非常低效 memory-consuming。 64 位会更糟。

对于缺少 FPU 的 MCU 上的浮点数也是如此,这些也会通过软件浮点库生成非常低效的代码。


为了说明,请看一下这个 non-optimized 代码,用于在 8 位 AVR (gcc) 上进行一些非常简单的 64 位加法运算:https://godbolt.org/z/ezbKjY
它实际上支持 uint64_t 但编译器产生了大量的开销代码,大约 100 条指令。在它的中间,调用隐藏在可执行文件中的内部编译器函数call __adddi3

如果我们启用优化,我们得到

add64:
        push r10
        push r11
        push r12
        push r13
        push r14
        push r15
        push r16
        push r17
        call __adddi3
        pop r17
        pop r16
        pop r15
        pop r14
        pop r13
        pop r12
        pop r11
        pop r10
        ret

我们将不得不深入研究库源代码或 single-step 程序集以查看 __adddi3 中有多少代码。我想这仍然不是一个微不足道的功能。

所以正如你所希望的那样,在 8 位 CPU 上进行 64 位算术是一个非常糟糕的主意。

好吧,如果您最关心的是保持相当程度的兼容性,这就是避免使用 64 位数字的原因,那么您为什么不使用 int 整数数组,并考虑使用一个要存储的完整整数,比方说,30 位。

我建议您查看有关使用位掩码(大于 32 位)来表示例如select(2) 系统调用涉及的文件,以及如何使用 FDSET 宏。

真实的情况是,您可能遇到了以下问题:决定是在用于表示位图的数据类型中超过 32 位的限制,还是通过使用仍然可用的 64 位类型(暂时)解决问题。当您使用 64 位位掩码时,这将是下一个规模问题,您最终将不得不越界。

你现在就可以做,作为练习,你将了解到最后的数据类型是或多或少的大位集,你可以随意使用它们。您打算使用 80 位 long double 值来存储大于 64 位的位掩码吗?我想你不会,所以想想阵列解决方案,这可能会一劳永逸地解决你的问题。

如果你的问题是我的情况,我会写一个 32 位无符号数数组,这样所有位在移位、位操作等方面的表现都是一样的。


#define FDSET_TYPE(name, N)  unsigned int name[((N) + 31U) >> 5]
#define FDSET_ISSET(name, N) ((name[(N) >> 5] & 1 << (N & 0x1f)) != 0)

...

    FDSET_TYPE(name, 126);

...

    if (FDSET_ISSET(name, 35)) { ...

在上面的示例中,FDSET_TYPE 宏允许您声明作为第二个参数传递的位数的变量,并使用无符号 32 位整数数组实现它,四舍五入到允许包含所有位的下一个值。 FDSET_ISSET(name, 35) 计算请求位所在的单元格和偏移量,并用您传递的数字除以 32 的余数对其进行掩码 --- 但是当我们选择 2 的幂时,y 使用掩码 0x1f 屏蔽数字的最后 5 位以获得余数 mod 32).