简体   繁体   中英

Inconveniences of using uint64_t

I have a highly portable library (it compiles and works well everywhere, even without a kernel) and I would like that it remains as portable as possible. So far I have avoided 64bit data types, but I might need to use them now – to be precise I would need a 64bit bitmask.

I have never really thought about it and I am not enough an hardware expert (especially concerning embedded systems), but I am wondering now: what are the inconveniences of using uint64_t (or, equivalently, uint_least64_t )? I can think of two approaches to my question:

  1. Actual portability: Are all microcontrollers – including 8bit CPU – able to deal with 64bit integers?
  2. Performance: How slow will a 8bit CPU perform bitwise operations on a 64bit integer compared to a 32bit integer? The function I am designing will have only one 64bit variable, but will perform a lot of bitwise operations on it (ie in a loop).

There are various minimum requirements on a conforming C compiler. The C language allows two forms of compilers: hosted and freestanding . Hosted is meant to run on top of an OS, and freestanding runs without an OS. Most embedded systems compilers are freestanding implementations.

Freestanding compilers have some leeway, they do not need to support all of the standard libraries, but they need to support a minimum subset of them. This includes stdint.h (see C17 4/6). Which in turn requires the compiler to implement the following (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

So a microcontroller compiler does not need to support uint64_t , but it must (oddly enough) support uint_least64_t . In practice it means that the compiler might as well add uint64_t support too, since it's the same thing in this case.

As for what a 8 bit MCU supports... it supports 8 bit arithmetic through the instruction set, in some special cases also a few 16 bit operations using index registers. But in general, it must rely on software libraries whenever a larger type than 8 bits is used.

So if you attempt 32 bit arithmetic on a 8 bitter, it will inline some compiler software libraries with the code and the result will be hundreds of assembler instructions, making such code very inefficient and memory-consuming. 64 bit will be even worse.

Same thing with floating point numbers on MCUs that lack a FPU, these too will generate horribly inefficient code through software floating point libraries.


To illustrate, take a look at this non-optimized code for some very simple 64 bit addition on an 8-bitter AVR (gcc): https://godbolt.org/z/ezbKjY
It actually supported uint64_t but the compiler spewed out an enormous amount of overhead code, some 100 instructions. And in the middle of it, a call to an internal compiler function call __adddi3 hidden in the executable.

If we enable optimizations, we get

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

We'll have to dig through the library source or single-step the assembly live to see how much code there is inside __adddi3 . I would guess it is not a trivial function still.

So as you hopefully can tell, doing 64 bit arithmetic on an 8-bit CPU is a very bad idea.

Well, if your primary concern is to maintain a fair level of compatibility, and that's the reason to avoid using 64bit number, why don't you use an array of int integers, and consider using one full integer to store, let's say, 30 bits.

I recommend you to have a look to standard library sources concerning the use of bit masks (larger than 32 bits) for representing eg the files touched by the select(2) system call, and how to use the FDSET macros.

What is true is that you are probably having the problem of deciding if crossing the limit of 32 bits in a data type used to represent bitmaps, or solving the problem (temporarily) by using the still available 64bit types. This will be a next scale problem when you get around 64bit bitmasks and the you'll finally have to cross the line.

You can do it now, as an exercise, and you'll learn that a data type on the end is a more or less large set of bits and you can give them any use you want. Do you plan to use 80bit long double values to store larger than 64bit bitmasks? I think you won't, so think on the array solution, that probably will solve your problem once and forever.

Should your problem be my case, I'd write an array of 32bit unsigned numbers, so all bits are equally behaved at shifts, bit operations and the like.


#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)) { ...

in the above example above, the FDSET_TYPE macro allows you to declare a variable of the number of bits you pass as second parameter, and implements it using an array of unsigned 32bit integers, rounded up to the next value to allow all bits to be included. The FDSET_ISSET(name, 35) calculates the cell and the offset where the requested bit resides and masks it with the remainder of dividing the number you pass by 32 --- but as we selected a power of two, y use a mask of 0x1f to mask the last 5 bits of the number to get the remainder mod 32).

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM