c-使用uint64_t的不便



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

我从来没有真正想过,我也不是一个足够的硬件专家(尤其是嵌入式系统),但我现在想知道:使用uint64_t(或等效地,uint_least64_t)有什么不便?我可以想出两种方法来回答我的问题:

  1. 实际可移植性:是否所有微控制器(包括8位CPU)都能处理64位整数
  2. 性能:与32位整数相比,8位CPU对64位整数执行逐位操作的速度有多慢?我正在设计的函数将只有一个64位变量,但将对其执行许多逐位操作(即在循环中)

对一致性C编译器有各种最低要求。C语言允许两种形式的编译器:托管独立。Hosted是指在操作系统上运行,而独立运行则没有操作系统。大多数嵌入式系统编译器都是独立的实现。

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

需要以下类型:

int_least8_t内部_东部16_tint_least32_tint_least64_t
uint_least8_tuint_list16_tuint_list32_tuint_list64_t

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

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

因此,如果你在一个8位的biter上尝试32位算术,它会将一些编译器软件库与代码内联,结果将是数百条汇编指令,这会使这些代码非常低效和消耗内存。64位会更糟。

与缺少FPU的MCU上的浮点数字相同,这些数字也会通过软件浮点库生成效率极低的代码。


为了说明这一点,我们来看看这个未优化的代码,它在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

我们必须深入了解库源代码或组装的单个步骤,以了解__adddi3中有多少代码。我想这仍然不是一个微不足道的函数。

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

我在Godbolt上使用Arduino Mega编译器测试了四种64位AND变体。

struct pair
{
uint32_t hi;
uint32_t lo;
};
struct quad
{
uint16_t w;
uint16_t x;
uint16_t y;
uint16_t z;
};
struct octuplet
{
uint8_t n1;
uint8_t n2;
uint8_t n3;
uint8_t n4;
uint8_t n5;
uint8_t n6;
uint8_t n7;
uint8_t n8;
};
uint64_t bitwiseAnd64(uint64_t bits, uint64_t mask)
{
return bits & mask;
}
pair bitwiseAndPairs(const pair& bits, const pair& mask)
{
return pair{bits.hi & mask.hi, bits.lo & mask.lo};
}
quad bitwiseAndQuads(const quad& bits, const quad& mask)
{
return quad{bits.w & mask.w, bits.x & mask.x,
bits.y & mask.y, bits.z & mask.z};
}
octuplet bitwiseAndOctuplets(const octuplet& bits, const octuplet& mask)
{
return octuplet{bits.n1 & mask.n1, bits.n2 & mask.n2,
bits.n3 & mask.n3, bits.n4 & mask.n4,
bits.n5 & mask.n5, bits.n6 & mask.n6,
bits.n7 & mask.n7, bits.n8 & mask.n8};
}

结果是:

  1. uint64_t操作数上的位AND:
    • 25组装说明
  2. uint32_t操作数对上的逐位"与">
    • 69组装说明
  3. uint16_t操作数的四元组进行逐位"与"运算。
    • 71组装说明
  4. uint8_t操作数的八元组进行逐位"与"运算。
    • 60组装说明

所以我没能打败编译器合成的64位"与"。请注意,按值传递structs会显著增加更多的指令。

如果你最需要做的是检查单个位是否被设置或重置,那么上面的测试不会很好地模拟你的用例。检查是否设置了单个位所需的工作量比计算整个逐位AND结果要少得多!

所以我尝试了5种方法来检查Godbolt上是否设置了64位中的一位。

struct pair
{
uint32_t hi;
uint32_t lo;
};
struct quad
{
uint16_t w;
uint16_t x;
uint16_t y;
uint16_t z;
};
struct octuplet
{
uint8_t n1;
uint8_t n2;
uint8_t n3;
uint8_t n4;
uint8_t n5;
uint8_t n6;
uint8_t n7;
uint8_t n8;
};
bool test64(uint64_t bits)
{
return (bits & 0x0000000000008000) != 0;
}
bool testPair(const pair& bits)
{
return (bits.lo & 0x00008000) != 0;
}
bool testQuad(const quad& bits)
{
return (bits.z & 0x8000) != 0;
}
bool testOctuplet(const octuplet& bits)
{
return (bits.n7 & 0x80) != 0;
}
typedef uint8_t Bytes[64];
bool testArray(const Bytes& bytes)
{
return bytes[15] != 0;
}

结果:

  1. 测试是否在uint64_t整数中设置了位
    • 7装配说明
  2. 测试一对uint32_t操作数中是否设置了位
    • 15组装说明
  3. 测试一个位是否设置在uint16_t操作数的四元组中。
    • 6组装说明
  4. 测试uint8_t操作数的八元组中是否设置了位。
    • 6组装说明
  5. 测试给定位置的数组字节是否为一:
    • 8组装说明

所以这个故事的寓意是:让编译器担心编译器支持的任何字长的逐位算术!

好吧,如果你主要关心的是保持公平的兼容性,这也是避免使用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)。

相关内容

  • 没有找到相关文章

最新更新