C语言 使用uint64_t的不便之处

d5vmydt9  于 2023-03-22  发布在  其他
关注(0)|答案(3)|浏览(591)

我有一个高度可移植的库(它在任何地方都能编译和工作,即使没有内核),我希望它尽可能保持可移植性。到目前为止,我一直避免使用64位数据类型,但我现在可能需要使用它们-准确地说,我需要一个64位位掩码。
我从来没有真正想过这个问题,我也不是一个硬件Maven(特别是关于嵌入式系统),但我现在想知道:使用uint64_t(或等价地,uint_least64_t)有什么不便之处?我可以想到两种方法来回答我的问题:
1.实际可移植性:是否所有微控制器(包括8位CPU)都能处理64位整数?
1.性能:一个8位的CPU对一个64位的整数执行按位操作的速度和对一个32位的整数执行按位操作的速度有多慢?我设计的函数只有一个64位的变量,但是会对它执行很多按位操作(即在一个循环中)。

cmssoen2

cmssoen21#

C语言允许两种形式的编译器:hostedfreestanding。Hosted意味着在操作系统上运行,而freestanding则不需要操作系统。大多数嵌入式系统编译器都是独立的实现。
独立编译器有一些灵活性,它们不需要支持所有的标准库,但它们需要支持它们的最小子集。这包括stdint.h(参见C17 4/6)。这反过来要求编译器实现以下内容(C17 7.20.1.2/3):
需要以下类型:
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位的bitter上尝试32位的算术,它会将一些编译器软件库与代码内联,结果将是数百条汇编指令,使得这样的代码非常低效和消耗内存。
同样的事情,在缺少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位运算是一个非常糟糕的想法。

a9wyjsp7

a9wyjsp72#

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

mwngjboj

mwngjboj3#

我已经使用Arduino Mega compiler on Godbolt测试了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};
}

结果如下:

  • uint64_t操作数的按位AND:
  • 25组装说明
  • uint32_t操作数对的逐段按位AND
  • 69组装说明
  • uint16_t操作数的四元组进行逐段按位AND运算。
  • 71组装说明
  • uint8_t操作数的八元组进行逐段按位AND运算。
  • 60组装说明

所以我无法击败编译器的合成64位按位AND。注意,按值传递结构体会明显增加更多的指令。
如果你主要需要做的是检查单个位是否被设置或重置,那么上面的测试不会很好地模拟你的用例。检查单个位是否被设置比计算整个按位AND结果需要的工作要少得多!
所以我尝试了5种方法来检查一个64位的集合中是否有一位是set on Godbolt

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;
}

结果:

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

所以这个故事的寓意是:让编译器为编译器支持的任何字长的按位运算操心吧!

相关问题