C Integer Promotion on 8-bit MCUs

Long story short:

The integer promotion to 16 bits always takes place - the C standard enforces this. But the compiler is allowed to optimize the calculation back down to 8 bits (embedded systems compilers are usually pretty good at such optimizations), if it can deduce that the sign will be the same as it would have been if the type had been promoted.

This is not always the case! Implicit signedness changes caused by integer promotion are a common source of bugs in embedded systems.

Detailed explanation can be found here: Implicit type promotion rules.


unsigned int fun1 ( unsigned int a, unsigned int b )
{
    return(a+b);
}

unsigned char fun2 ( unsigned int a, unsigned int b )
{
    return(a+b);
}

unsigned int fun3 ( unsigned char a, unsigned char b )
{
    return(a+b);
}

unsigned char fun4 ( unsigned char a, unsigned char b )
{
    return(a+b);
}

as expected fun1 is all ints so does the 16 bit math

00000000 <fun1>:
   0:   86 0f           add r24, r22
   2:   97 1f           adc r25, r23
   4:   08 95           ret

Although technically incorrect as it is a 16 bit addition called out by the code, even unoptimized this compiler removed the adc due to the result size.

00000006 <fun2>:
   6:   86 0f           add r24, r22
   8:   08 95           ret

not really surprised here the promotion happens, compilers didnt used to do this not sure what version made this start happening, ran into this early in my career and despite the compilers promoting out of order (just like above), doing the promotion even though I told it to do uchar math, not surprised.

0000000a <fun3>:
   a:   70 e0           ldi r23, 0x00   ; 0
   c:   26 2f           mov r18, r22
   e:   37 2f           mov r19, r23
  10:   28 0f           add r18, r24
  12:   31 1d           adc r19, r1
  14:   82 2f           mov r24, r18
  16:   93 2f           mov r25, r19
  18:   08 95           ret

and the ideal, I know it is 8 bit, want an 8 bit result so I simply told it to do 8 bit all the way through.

0000001a <fun4>:
  1a:   86 0f           add r24, r22
  1c:   08 95           ret

So in general it is better to aim for the register size, which is ideally the size of an (u)int, for an 8 bit mcu like this the compiler authors had to make a compromise...Point being dont make a habit of using uchar for math that you know doesnt need more than 8 bits as when you move that code or write new code like that on a processor with larger registers now the compiler has to start masking and sign extending, which some do natively in some instructions, and others dont.

00000000 <fun1>:
   0:   e0800001    add r0, r0, r1
   4:   e12fff1e    bx  lr

00000008 <fun2>:
   8:   e0800001    add r0, r0, r1
   c:   e20000ff    and r0, r0, #255    ; 0xff
  10:   e12fff1e    bx  lr

forcing 8 bit cost more. I cheated a little/lot, would need slightly more complicated examples to see more of this in a fair way.

EDIT based on comments discussion

unsigned int fun ( unsigned char a, unsigned char b )
{
    unsigned int c;
    c = (a<<8)|b;
    return(c);
}

00000000 <fun>:
   0:   70 e0           ldi r23, 0x00   ; 0
   2:   26 2f           mov r18, r22
   4:   37 2f           mov r19, r23
   6:   38 2b           or  r19, r24
   8:   82 2f           mov r24, r18
   a:   93 2f           mov r25, r19
   c:   08 95           ret

00000000 <fun>:
   0:   e1810400    orr r0, r1, r0, lsl #8
   4:   e12fff1e    bx  lr

no surprise. Although why did the optimizer leave that extra instruction, can you not use ldi on r19? (I knew the answer when I asked it).

EDIT2

for avr

avr-gcc --version
avr-gcc (GCC) 4.9.2
Copyright (C) 2014 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

to avoid the bad habit or not 8 bit comparison

arm-none-eabi-gcc --version
arm-none-eabi-gcc (GCC) 7.2.0
Copyright (C) 2017 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

clearly optimization was on only takes a second to try with your own compiler to see how it compares to my output, but anyway:

whatever-gcc -O2 -c so.c -o so.o
whatever-objdump -D so.o

And yes using bytes for byte sized variables, certainly on an avr, pic, etc, will save you memory and you want to really try to conserve it...if you are actually using it, but as shown here as little as possible is going to be in memory, as much in registers as possible, so the flash savings comes by not having extra variables, ram savings may or may not be real..


Not necessarily, since modern compilers do a good job at optimizing generated code. For example, if you write z = x + y; where all variables are unsigned char, the compiler is required to promote them to unsigned int before performing the calculations. However, since the end result will be exactly the same without the promotion, the compiler will generate code which just adds 8-bit variables.

Of course, this is not always the case, for example the result of z = (x + y)/2; would depend on the upper byte, so promotion will take place. It can still be avoided without resorting to assembly by casting the intermediate result back to unsigned char.

Some of such inefficiencies can be avoided using compiler options. For example, many 8-bit compilers have a pragma or a command-line switch to fit enumeration types in 1 byte, instead of int as required by C.