Difference between uint8_t, uint_fast8_t and uint_least8_t

31,923

Solution 1

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.

Solution 2

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.

Solution 3

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.

Solution 4

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.

Solution 5

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

Share:
31,923
mic
Author by

mic

Updated on July 08, 2022

Comments

  • mic
    mic almost 2 years

    The C99 standard introduces the following datatypes. The documentation can be found here for the AVR stdint library.

    • uint8_t means it's an 8-bit unsigned type.
    • uint_fast8_t means it's the fastest unsigned int with at least 8 bits.
    • uint_least8_t means it's an unsigned int with at least 8 bits.

    I understand uint8_t and what is uint_fast8_t( I don't know how it's implemented in register level).

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

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

  • user541686
    user541686 over 8 years
    I love how you have to imagine exotic architectures to find a use case for this. Have they found any usefulness in practice?
  • user694733
    user694733 over 8 years
    @Mehrdad In ARM for example, if your int_fast8_t is 32-bit variable, you don't need to do sign extension before arithmetric operations.
  • legends2k
    legends2k over 8 years
    Thanks. Good to know that the signed types in stdint.h are guaranteed to be two's complement. Wonder where it will help when writing portable code.
  • skyking
    skyking over 8 years
    Note that only the exact width variants are required to use 2's complement format. Also note that these are not required to exist. Consequently a platform is not required to support 2's complement format.
  • skyking
    skyking over 8 years
    @Mehrdad MIPS for example it would be very wrong to make any uintX_fast_t less than 32 bits. You don't even have to imagine architectures to get uint8_t to be undefined, take for example UNIVAC which is 36-bit, I'd assume that there char is 9-bit.
  • user541686
    user541686 over 8 years
    @skyking: I was asking whether the types have been useful on any architectures. In other words, do people actually use them and benefit from their semantics across architectures? Or are they just gathering dust or being used when they are not actually needed or useful?
  • rodrigo
    rodrigo over 8 years
    @Mehrdad: I admit that I've never seen the uint_leastX_t or uint_fastX_t used in real world applications. uintX_t yes, they are heavily used. It looks like people are not very interesting in portability to exotic architectures. Which is expected, even if you get your unsigneds right, your program will fail at a thousand different things.
  • M.M
    M.M over 8 years
    int_fast32_t is plausible: for situations where you'd normally use int but don't want it to break on 16-bit platform
  • skyking
    skyking over 8 years
    @Mehrdad I'd say it's quite useful, at least the *int_least*_t and uint_fast*_t. They are guaranteed to exist and guaranteed to have the stated range - this means that the code actually becomes portable. Of course one could argue that one could use int instead of int_least16_t and long instead of int_least32_t (but note how this makes you use 32-bit numbers instead of 16-bit where 16-bit is available, or 64-bit numbers instead of 32-bit where 32-bit is available).
  • rodrigo
    rodrigo over 8 years
    @Mehrdad: ...and if you are programming for an exotic architecture, then you know your types, you are unlikely to write code useful for another computer, so you don't care about portability.
  • skyking
    skyking over 8 years
    @rodrigo Why would one not care about portability? My experience is that people on mainstream architecture are more inclined to assume that every platform that behaves slightly different is "broken" and that one would not have to care about int not being suitable to hold a pointer.
  • rodrigo
    rodrigo over 8 years
    @skyking: Sure they are useful, in theory. But I did a quick grep through the source code of a few OS popular programs and libraries I have around, and the only uses of these types are in embedded copies of stdint.h. And this header is only required for the uintX_t ones.
  • user694733
    user694733 over 8 years
  • rodrigo
    rodrigo over 8 years
    @skyking: I'm not saying they should not be used, just that they are not used very much in practice. If you can find a real-world application or library that uses them sensibly, then post a link, because I couldn't find any.
  • skyking
    skyking over 8 years
    @rodrigo Yes, in theory one should be concerned with integer overflow, but in practice it seems that programmers just assume that those does not happen (then comes Y2K that shouldn't happen).
  • user541686
    user541686 over 8 years
    For anyone for whom it might have been unclear -- what I've been trying to say earlier are the following two things: (1) Yes, I have seen some ultra-correct people use [u]int_{least,fast}*_t in their code, but (2) I have yet to see anyone do this because it has provided them with an actual benefit, rather than a hypothetical one. In other words, in the use cases I've seen, the people would have (in practice) been just as well off using unsigned int or unsigned char or size_t or whatever.
  • DevSolar
    DevSolar over 8 years
    @Mehrdad: The advantage is in context. If you write unsigned int, it is not clear why you used that type (and not some other). Is it because it's exactly 32 bit, or because it's at least 32 bit? Or is it because that's the "fastest" type? (On your platform...) I -- as a maintenance coder -- don't know your intentions in using unsigned int, and I will have doubts that it might be the wrong type in the context (and thus responsible for the bug I am hunting). If you wrote uint_least32_t, your intention is clear, and I can double-check if your assumptions were / are correct.
  • Florian Keßeler
    Florian Keßeler over 8 years
    @merhad: I've worked with a CPU that only had 32 bit values for everything. (Actually 40 bits, but the compiler made everything into 32 bit values), so it had no uint8_t or uint16_t, but uint32_t and all the uint_least* and uint_fast* types were the same as uint32_t. This was an ADSP SHARC. In my current project we are using the fast and least types a lot.
  • zwol
    zwol over 8 years
    glibc's definitions were chosen at a time when those optimizations didn't exist, and they are now baked into the ABI and cannot be changed. This is one of the several reasons why the _least and _fast types are not actually useful in practice.
  • supercat
    supercat over 8 years
    @zwol: I wish the language would add types types that were defined in terms of layout and semantic requirements, e.g. "I need something whose lower bits will alias other 16-bit types, and which can hold values 0-65535, but I don't need it to peg larger values to that range". Aliasing, layout, range, and out-of-range behavior should be four separate aspects of a type, but C only allows certain combinations which aren't consistent among different platforms.
  • supercat
    supercat over 8 years
    @DevSolar: Unfortunately, the semantics of the least and fast types in mixed-type expressions are a total mess. For example, given uint_least8_t x=1; what should be the value of x-2 > 5?
  • supercat
    supercat over 8 years
    @legends2k: The types in stdint.h are rather less helpful than one might like if one is trying to write portable code, since while they are required to use two's-complement storage format, that does not imply that they will exhibit two's-complement wrapping behavior. Note also that even on platforms where int is 32 bits, writing a value using an int32_t* and reading using an int*, or vice versa, is not guaranteed to work.
  • DevSolar
    DevSolar over 8 years
    @supercat: "Don't rely on integer overflow / underflow semantics, unsigned or not"?
  • supercat
    supercat over 8 years
    @DevSolar: With uint8_t there would be no integer overflow, since 2 is signed. Perhaps it would be more helpful to have separate least "number" and "wrapping algebraic ring" types, so a unum_least8_t could be any type--signed or unsigned--that can hold 0-255 and promotes to a signed type while uwrap_least8_t would be a type which promotes to something which when added, subtrated, multiplied, etc. will yield a result whose bottom 8 bits will be as though the operation were performed on an 8-bit type.
  • Lundin
    Lundin over 8 years
    @supercat Every compiler I have seen use an internal typedef for the stdint.h types, to make them synonymous with one of the basic "keyword" integer types. So if you worry about pointer aliasing I don't think that's going to be an issue in practice, only in theory.
  • supercat
    supercat over 8 years
    @Lundin: Some compilers use "long" as the typedef for int32_t, and some use "int". Even when "int" and "long" have the same representation they may be (and sometimes are) considered distinct for purposes of C's aliasing rules.
  • Lundin
    Lundin over 8 years
    @supercat Wouldn't it be quite stupid of the compiler to typedef it as a type that causes conflict with each own aliasing handling? Seems like a problem for platform-independent compilers only (GCC), where the compiler port for a given hardware has no control over how pointer aliasing is done?
  • supercat
    supercat over 8 years
    @Lundin: Any particular compiler will likely define "int32_t" and "uint32_t" to "int" and "unsigned int", or to "long" and "unsigned long". The problem is that on platforms where "int" and "long" are both 32 bits, there's no particular reason to expect "int32_t" to be one size or the other. If one is passing arrays to libraries which use *int, *long, and *int32_t, there's no way to make one array be compatible with all three even on platforms where all three types have the same size and representation.
  • Krazy Glew
    Krazy Glew over 8 years
    Exotic architectures, like machines with 9-bit bytes, make this easy to understand. (By the way, at least one widespread ESP has 9 bit bytes, and 36-bit "words" in registers. The extra bits are used as guard bits to prevent overflow/saturation. They are thrown away when stored to memory.) // But, this applies even to common RISCs with 32-bit reg-reg instructions. uint8_t would require truncation - eg by AND or by store -byte/load-byte - after every uint8_t operation. I think that uint_fast8_t is allowed to sometimes truncate, and sometimes not, even of sizeof(uint8_t) == sizeof(uint_fast8_t).
  • plugwash
    plugwash about 7 years
    "For example, given uint_least8_t x=1; what should be the value of x-2 > 5?" 1 if char is the same size as int 0 if char is smaller than int.
  • rodrigo
    rodrigo about 7 years
    @plugwash: x is integral promoted before the substraction, so if its type is the same size as int it will be promoted to unsigned int and the result will be true. If it is smaller it will be promoted to int and the result will be false.
  • Euri Pinhollow
    Euri Pinhollow almost 7 years
    @supercat if you write uint[ANY]_t a=1; a-2; you are using implementation-defined behaviour and it is clearly stated in standard.
  • supercat
    supercat almost 7 years
    @EuriPinhollow: Implementations are free to support uint16_t or not, making the existence of uint16_t is Implementation Defined, but uint16_t a=1; a-=2; will set a to 65535 on any conforming implementation that supports uint16_t doesn't invoke the "One Program Rule" to justify doing something arbitrarily different.
  • Euri Pinhollow
    Euri Pinhollow almost 7 years
    @supercat you are right, standard actually says that: eel.is/c++draft/conv.integral#2 even though not only two's complement is allowed.
  • supercat
    supercat almost 7 years
    @EuriPinhollow: The conversion from signed types to unsigned is defined such that subtracting any positive N from any unsigned type and then adding N will yield the original value when the result is coerced to the original unsigned type [I say a positive value, because one could contrive a weird system where int had 16 value bits, a sign bit, and 15 bits of padding, and unsigned had 16 value bits and 16 bits of padding, and uint16_t ranked below int, where subtracting -1 from a uint16_t would yield Undefined Behavior]. On the other hand, ...
  • supercat
    supercat almost 7 years
    ...so far as I'm aware no C implementations that don't use two's-complement have ever supported 64-bit or larger unsigned type, which would make it unlikely that any conforming non-two's-complement C99 or C11 implementations will ever exist.
  • Galaxy
    Galaxy almost 6 years
    The extra potentially unnecessary instructions when dealing with a smaller data type vs a larger data type of the native word size go a long way to illustrate why many of the "fast" data types may have a greater bit width than expected. Thanks you for your example.
  • supercat
    supercat almost 6 years
    @Galaxy: Unfortunately, the Standard does not allow for the possibility of "least" types whose behavior could vary depending upon context. On many machines, for example, arithmetic on 32-bit values in registers may be faster than operations using 8-bit values in registers, but 8-bit loads and stores would be the same speed as 32-bit loads and stores, and caching issues may cause 8-bit values to be more efficient.
  • puchu
    puchu over 5 years
    If you are working for POSIX systems your don't need least types, you can use uintn_t directly. POSIX systems has a requirement CHAR_BIT == 8. I am just using fast and precise types.
  • Sara J
    Sara J over 2 years
    The intN_t types are guaranteed to be two's complement. But there doesn't seem like there's any such requirement for the int_leastN_t and int_fastN_t types, intptr_t or intmax_t