12
\$\begingroup\$

Using avr-gcc as an example, int types are specified to be 16-bits wide. Performing operations on 8-bit operands in C results in those operands being converted to 16-bit int types due to integer promotion in C. Does this mean that all 8-bit arithmetic operations on an AVR will take much longer if written in C than if written in assembly due to C's integer promotion?

\$\endgroup\$
  • 1
    \$\begingroup\$ I don't think so, the compiler will realize that the destination variable is an (unsigned) char, therefore it won't bother in calculating the top 8 bits. Still, I found that GCC sometimes is not so good in optimzing code, therefore if you code in ASM, the result MGIHT be faster. However, unless you're doing very time-critical tasks/interrutps, with a very strong budget constraint, then either you should choose a more powerful processor and program it in C, or just do not worry about the lower performance (consider instead time-to-market, better code readability/reusability, fewer bugs, ecc..). \$\endgroup\$ – next-hack Sep 7 '17 at 11:33
  • \$\begingroup\$ I apologise for not having time to check. However, I think there was a command-line flag to gcc which would control 'integer promotion'. There maybe even a pragma to control it for specific pieces of code. How critical is performance? In a lot of uses of an AVR, the difference in speed for some arithmetic is not an issue. Foxus on getting the code working correctly first. Then if there is a performance issue, find out what it is. It would be easy to waste time coding in assembler, only yo find it didn't matter. \$\endgroup\$ – gbulmer Sep 7 '17 at 11:36
  • 1
    \$\begingroup\$ just disassemble and see what the compiler is doing. From a pure language perspective yes. the implementation here is atypical. normally int attempts to align itself with the register size, and if you had 16 bit registers then 8 bit math is actually cheaper at 16 bit than 8. But this is the other way around and with an 8 bit mcu it makes sense to implement int as 16 bit. so you should probably use uchar where you care about this but dont make that a common programming habit as it hurts you most everywhere else. \$\endgroup\$ – old_timer Sep 7 '17 at 11:42
  • 3
    \$\begingroup\$ Remember: Avoid answering questions in comments. \$\endgroup\$ – pipe Sep 7 '17 at 11:49
  • 4
    \$\begingroup\$ These kind of questions are better to ask the C experts at SO, since it is a pure software question. Integer promotion in C is a somewhat complex topic - the average C programmer will have lots of misconceptions about it. \$\endgroup\$ – Lundin Sep 7 '17 at 13:00
14
\$\begingroup\$

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.

\$\endgroup\$
7
\$\begingroup\$

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.

\$\endgroup\$
  • 4
    \$\begingroup\$ "the compiler is required to promote them to unsigned int". No, the compiler is required to promote them to int, since char will most likely not have the same conversion rank as int on any platform. \$\endgroup\$ – Lundin Sep 7 '17 at 12:33
  • 3
    \$\begingroup\$ "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." The C standard does allow enumeration variables to be allocated in 1 byte. It only requires that enumeration constants must be int (yes it is inconsistent). C11 6.7.2.2 Each enumerated type shall be compatible with char, a signed integer type, or an unsigned integer type. The choice of type is implementation-defined... \$\endgroup\$ – Lundin Sep 7 '17 at 12:54
7
\$\begingroup\$
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..

\$\endgroup\$
  • 2
    \$\begingroup\$ "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." It is because embedded systems C compilers used to have horrible standard conformance :) The compiler is usually allowed to optimize, but here it can't deduct that the result will fit in an unsigned char so it has to perform the promotion to 16 bits, as required by the standard. \$\endgroup\$ – Lundin Sep 7 '17 at 12:47
  • 1
    \$\begingroup\$ @old_timer (a<<8)|b is always wrong for any system where int is 16 bits. a will get implicitly promoted to int which is signed. In case a held a value in the MSB, you end up shifting that data into the sign bit of a 16 bit number, which invokes undefined behavior. \$\endgroup\$ – Lundin Sep 7 '17 at 13:38
  • 1
    \$\begingroup\$ fun3 is fun..ny... totally unoptimized by the compiler... Considered that r1 is always 0 in GCC, and indicating ra, rb, {rh,rl} the registers for the variables a, b, and the result, the compiler could have done: 1) mov rh,r1; 2) mov rl,ra; 2) add rl,rb; 3) adc rh,rh; 4) ret. 4 Instructions, vs 7 or 8... Instruction 1 can be changed in ldi rh,0. \$\endgroup\$ – next-hack Sep 7 '17 at 14:25
  • 1
    \$\begingroup\$ This would be a better answer if it specified the compiler and relevant options in use. \$\endgroup\$ – Russell Borogove Sep 7 '17 at 15:56
  • 1
    \$\begingroup\$ It's a good idea to avoid using int/char etc. and instead use the much more explicit and readable int16_t and int8_t. \$\endgroup\$ – user Sep 8 '17 at 11:15

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.