Uint8_t、 uint_fast8_t 与 uint_least8_t 的区别

C99标准引入了以下数据类型。

  • uint8_t表示它是8位无符号类型。
  • uint_fast8_t意味着它是最快的无符号整型,至少有8个 比特。
  • uint_least8_t表示它是一个至少有8位的无符号整型数。

我了解 uint8_t和什么是 uint_fast8_t(我不知道它是如何在寄存器级实现的)。

1. 你能解释一下“它是一个至少有8位的 unsigned int”是什么意思吗?

2. 与 uint8_t相比,uint_fast8_tuint_least8_t如何帮助提高效率/代码空间?

38148 次浏览

uint_least8_t is the smallest type that has at least 8 bits. uint_fast8_t is the fastest type that has at least 8 bits.

You can see the differences by imagining exotic architectures. Imagine a 20-bit architecture. Its unsigned int has 20 bits (one register), and its unsigned char has 10 bits. So sizeof(int) == 2, but using char types requires extra instructions to cut the registers in half. Then:

  • uint8_t: is undefined (no 8 bit type).
  • uint_least8_t: is unsigned char, the smallest type that is at least 8 bits.
  • uint_fast8_t: is unsigned int, because in my imaginary architecture, a half-register variable is slower than a full-register one.

uint8_t means: give me an unsigned int of exactly 8 bits.

uint_least8_t means: give me the smallest type of unsigned int which has at least 8 bits. Optimize for memory consumption.

uint_fast8_t means: give me an unsigned int of at least 8 bits. Pick a larger type if it will make my program faster, because of alignment considerations. Optimize for speed.

Also, unlike the plain int types, the signed version of the above stdint.h types are guaranteed to be 2's complement format.

The "fast" integer types are defined to be the fastest integer available with at least the amount of bits required (in your case 8).

A platform can define uint_fast8_t as uint8_t then there will be absolutely no difference in speed.

The reason is that there are platforms that are slower when not using their native word length.

1.Can you explain what is the meaning of "it's an unsigned int with at least 8 bits"?

That ought to be obvious. It means that it's an unsigned integer type, and that it's width is at least 8 bits. In effect this means that it can at least hold the numbers 0 through 255, and it can definitely not hold negative numbers, but it may be able to hold numbers higher than 255.

Obviously you should not use any of these types if you plan to store any number outside the range 0 through 255 (and you want it to be portable).

2.How uint_fast8_t and uint_least8_t help increase efficiency/code space compared to the uint8_t?

uint_fast8_t is required to be faster so you should use that if your requirement is that the code be fast. uint_least8_t on the other hand requires that there is no candidate of lesser size - so you would use that if size is the concern.


And of course you use only uint8_t when you absolutely require it to be exactly 8 bits. Using uint8_t may make the code non-portable as uint8_t is not required to exist (because such small integer type does not exist on certain platforms).

The theory goes something like:

uint8_t is required to be exactly 8 bits but it's not required to exist. So you should use it where you are relying on the modulo-256 assignment behaviour* of an 8 bit integer and where you would prefer a compile failure to misbehaviour on obscure architectures.

uint_least8_t is required to be the smallest available unsigned integer type that can store at least 8 bits. You would use it when you want to minimise the memory use of things like large arrays.

uint_fast8_t is supposed to be the "fastest" unsigned type that can store at least 8 bits; however, it's not actually guaranteed to be the fastest for any given operation on any given processor. You would use it in processing code that performs lots of operations on the value.

The practice is that the "fast" and "least" types aren't used much.

The "least" types are only really useful if you care about portability to obscure architectures with CHAR_BIT != 8 which most people don't.

The problem with the "fast" types is that "fastest" is hard to pin down. A smaller type may mean less load on the memory/cache system but using a type that is smaller than native may require extra instructions. Furthermore which is best may change between architecture versions but implementers often want to avoid breaking ABI in such cases.

From looking at some popular implementations it seems that the definitions of uint_fastn_t are fairly arbitrary. glibc seems to define them as being at least the "native word size" of the system in question taking no account of the fact that many modern processors (especially 64-bit ones) have specific support for fast operations on items smaller than their native word size. IOS apparently defines them as equivalent to the fixed-size types. Other platforms may vary.

All in all if performance of tight code with tiny integers is your goal you should be bench-marking your code on the platforms you care about with different sized types to see what works best.

* Note that unfortunately modulo-256 assignment behaviour does not always imply modulo-256 arithmetic, thanks to C's integer promotion misfeature.

Some processors cannot operate as efficiently on smaller data types as on large ones. For example, given:

uint32_t foo(uint32_t x, uint8_t y)
{
x+=y;
y+=2;
x+=y;
y+=4;
x+=y;
y+=6;
x+=y;
return x;
}

if y were uint32_t a compiler for the ARM Cortex-M3 could simply generate

add r0,r0,r1,asl #2   ; x+=(y<<2)
add r0,r0,#12         ; x+=12
bx  lr                ; return x

but since y is uint8_t the compiler would have to instead generate:

add r0,r0,r1          ; x+=y
add r1,r1,#2          ; Compute y+2
and r1,r1,#255        ; y=(y+2) & 255
add r0,r0,r1          ; x+=y
add r1,r1,#4          ; Compute y+4
and r1,r1,#255        ; y=(y+4) & 255
add r0,r0,r1          ; x+=y
add r1,r1,#6          ; Compute y+6
and r1,r1,#255        ; y=(y+6) & 255
add r0,r0,r1          ; x+=y
bx  lr                ; return x

The intended purpose of the "fast" types was to allow compilers to replace smaller types which couldn't be processed efficiently with faster ones. Unfortunately, the semantics of "fast" types are rather poorly specified, which in turn leaves murky questions of whether expressions will be evaluated using signed or unsigned math.

I'm using the fast datatypes (uint_fast8_t) for local vars and function parameters, and using the normal ones (uint8_t) in arrays and structures which are used frequently and memory footprint is more important than the few cycles that could be saved by not having to clear or sign extend the upper bits. Works great, except with MISRA checkers. They go nuts from the fast types. The trick is that the fast types are used through derived types that can be defined differently for MISRA builds and normal ones.

I think these types are great to create portable code, that's efficient on both low-end microcontrollers and big application processors. The improvement might be not huge, or totally negligible with good compilers, but better than nothing.

Some guessing in this thread. "fast": The compiler should place "fast" type vars in IRAM (local processor RAM) which requires fewer cycles to access and write than vars stored in the hinterlands of RAM. "fast" is used if you need quickest possible action on a var, such as in an Interrupt Service Routine (ISR). Same as declaring a function to have an IRAM_ATTR; this == faster access. There is limited space for "fast" or IRAM vars/functions, so only use when needed, and never persist unless they qualify for that. Most compilers will move "fast" vars to general RAM if processor RAM is all allocated.

As the name suggests, uint_least8_t is the smallest type that has at least 8 bits, uint_fast8_t is the fastest type that has at least 8 bits. uint8_t has exactly 8 bits, but it is not guaranteed to exist on all platforms, although this is extremely uncommon.

In most case, uint_least8_t = uint_fast8_t = uint8_t = unsigned char. The only exception I have seen is the C2000 DSP from Texas Instruments, it is 32-bit, but its minimum data width is 16-bit. It does not have uint8_t, you can only use uint_least8_t and uint_fast8_t, they are defined as unsigned int, which is 16-bit.