What are the differences between the variants of uintX_t

Just something I would like to know about this.
What is the difference between these ? uint8_t just being an example. I would imagine that the others 16,32,64, would be the same.

uint8_t , uint_fast8_t and uint_least8_t

Are they all 2's complement negative numbers, or is that just uint8_t ?

uint8_t is an unsigned integer of 8 bit
uint_fast8_t allows the compiler to use the fastest type but not smaller than 8 bit. On a 32 bit CPU, a 32 bit integer will be faster than a 8 bit integer because it can be taken from memory and put in registers in one go.
uint_least8_t allows the compiler to use other sizes, but no smaller than 8 bit. This might lead to less code (as casts from 8 to 32 bit and back from 32 to 8 bit will not be needed).
In both cases you should not rely on rollovers and bit shift out.
If uint8_t is used:
(128<<1)>>1 will give 0 as a result.
But if uint16_t type is used, this will give:
1 as a result.
(example changed as reaction on post 3)

NONE of them are negative numbers. They are ALL u(nsigned) (i.e. - positive-only) int(egers), having 8, 16, or 32-bits, as indicated by their types.

For 8-bit integers, this happens to be true because of integral promotion, but in general, keep in mind that

if the value of the right operand is negative or is greater or equal to the number of bits in the promoted left operand, the behavior is undefined.
Arithmetic operators - cppreference.com

PieterP, thanks for pointing that out. I adapted the example in post 2.

Maybe this is a better example:
(255+1)/2=0
If uint8t is used, but
(255+1)/2=128
If uint16 is used.

Thanks for the info, makes it a little clearer now.

Not sure about the 32 bit register answer given that writing an 8 bit value is also written in one step, unless the compiler is doing something with an 8 bit number and making it a two step by padding out to the available register size ? That would explain it being faster.

I did find this: Fixed width integer types (since C++11) - cppreference.com

tells you what they are but no real explanation.

if you note it states that int16_t , with no padding bits and using 2's complement for negative values

For signed integers, the 8-bit value might have to be sign-extended to load it into a 32-bit or 64-bit register and use it with 32-bit instructions.

For unsigned integers, the intermediate results might be carried out in 32-bit or 64-bit arithmetic, in which case the result has to be reduced modulo 2N.

As a concrete example, consider:

#include <cstdint>

void i8div(int8_t &a, const int8_t &b) {
    a /= b;
}

void i32div(int32_t &a, const int32_t &b) {
    a /= b;
}

On an ESP32, this compiles to:

i8div(signed char&, signed char const&):
        entry   sp, 32     ; enter function
        l8ui    a8, a2, 0  ; load a
        l8ui    a9, a3, 0  ; load b
        sext    a8, a8, 7  ; sign extend a
        sext    a9, a9, 7  ; sign extend b
        quos    a8, a8, a9 ; divide
        s8i     a8, a2, 0  ; store quotient in a
        retw.n             ; return from function
i32div(int&, int const&):
        entry   sp, 32     ; enter function
        l32i.n  a8, a2, 0  ; load a
        l32i.n  a9, a3, 0  ; load b
        quos    a8, a8, a9 ; divide
        s32i.n  a8, a2, 0  ; store quotient in a
        retw.n             ; return from function

Notice how the 8-bit version includes additional sext instructions (sign extend).

An example for unsigned integers on ARM:

uint8_t foo8(uint8_t a, uint8_t b) {
    uint8_t c = a + b;
    return c * c;
}

uint32_t foo32(uint32_t a, uint32_t b) {
    uint32_t c = a + b;
    return c * c;
}
foo8(unsigned char, unsigned char):
        and     w1, w1, 255      ; b = b mod 256
        add     w0, w1, w0, uxtb ; c = a + b (with zero extension of byte)
        and     w0, w0, 255      ; c = c mod 256
        mul     w0, w0, w0       ; c * c
        ret
foo32(unsigned int, unsigned int):
        add     w0, w0, w1 ; c = a + b
        mul     w0, w0, w0 ; c * c
        ret

Here, notice the additional and instructions to reduce the intermediate values modulo 256.

Generally, all C primitive types follow the textbook definitions and behaviours of the types they are designed to represent. Thus if you learn those (either by research or by studying comp-sci) the behaviour is not difficult to understand and complex code examples do not demonstrate aspects of C, rather just aspects of the abstract data types.

Some things have been left undefined, for good reason, since C was designed to be "close to the hardware" and different hardware may implement some data types using different ADT's, for example signed values.

Obviously the hardware can't always be the same, or the language wouldn't be portable. But it exploits commonality in the processor native data types.

stdint.h removed ambiguity about the size (# bytes) of variables

and made standard what was already become a standard practice

Interesting. foo8 seems to believe that one or both parameters (w0 or w1) may contain a value larger than 255, so it masks both parameters down to 8-bit values. Then the function goes ahead and returns with possibly some of bits 8-15 set in w0 (apparently callers must ignore all bits besides 0-7 in w0, since the function returns uint8_t).

But the thing about multiplication is that, even if the parameters have any bits above bit 7 set, the low 8 bits of the product will still be correct (any bits above bit 7 can affect only bits 8-31 in the product, which don't matter). So the two and instructions seem to be completely unnecessary, and foo8 could potentially be the same size and speed as foo32. That seems like a missed optimization of the compiler.

Has the data type int been formed/derived by taking the first three letters of the word integers?

Would that surprise you?

Yes! I know that int is a keyword and not the integers?

Is it the correct assembly syntax for mul instruction of ATmega328P MCU?

So?
Float is floating point representation
Char is character
Are those coincidences?

float is the keyword which refers to the IEEE-754/binary32 standard for the representation of floating point number.

char is a keyword; whereas, character refers to a letter of English Language Alphabet (0 - 9, A - Z, a - z, punctuation marks, and additionally control characters).

So? What does all that add to answering your question?
Is int derived from integer?
Presumably yes, but we should ask the guy that first introduced this somewhere in the sixties to be really really sure.
For me it works to simply believe it is...

1 Like

Yes, of course is has! int8_t is 8-bit integer type. uint16_t is 16-bit integer unsigned int type. The fact that int is a c keyword is irrelevent.

Thanks for all the replies.
I can see this is very MPU/CPU orientated, so depends on the architecture you are using.

You can see why something like this is needed, given in the past I used char to define a byte wide number, for a target 8 bit MPU, rather than any of the other types, which are variable dependant on which processor unit is used. removing the ambiguity is a much needed thing, especially with embedded.

Arduino is 8 bit register and memory and int8_t is appropriate for the storage type. I know that the ALU uses 2, 8 bit registers for calculation and stores the higher byte in the first register, with the lower byte in the next register.

I still have no idea what fast would mean in the context of the 8 bit register, but higher register sizes are explained.

The Arduino registers are 8 bit wide so in terms of assembler you would just write an 8 bit number straight into a register, the only reason I can see for the differences are compiler specific directives. Take the case of something being defined as uint16_t, the compiler would know to allocate two consecutive memory locations to hold this data, rather than some variable width which may be up to a 32 bit long int, thus using 4 locations and wasting memory space.

The last thing is there is a differentiation between int and uint which should be taken into account with the loss of the 1 bit precision when using one over the other.

Nothing. But the point of these types is efficient portability across different systems, not just 8-bit microcontrollers.

On an AVR, uint8_t and uint_fast8_t have the same size:

#include <stdint.h>
static_assert(sizeof(uint8_t) == sizeof(uint_fast8_t), "");

In fact, they're even aliases for the same type:

template <class T, class U> // https://en.cppreference.com/w/cpp/types/is_same
struct is_same { constexpr static bool value = false; };
template <class T>
struct is_same<T, T> { constexpr static bool value = true; };

static_assert(is_same<uint8_t, uint_fast8_t>::value, "");