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?
-
2\$\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– next-hack2017年09月07日 11:33:32 +00:00Commented Sep 7, 2017 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– gbulmer2017年09月07日 11:36:02 +00:00Commented Sep 7, 2017 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– old_timer2017年09月07日 11:42:27 +00:00Commented Sep 7, 2017 at 11:42
-
3\$\begingroup\$ Remember: Avoid answering questions in comments. \$\endgroup\$pipe– pipe2017年09月07日 11:49:27 +00:00Commented Sep 7, 2017 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– Lundin2017年09月07日 13:00:05 +00:00Commented Sep 7, 2017 at 13:00
3 Answers 3
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..
-
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– Lundin2017年09月07日 12:47:37 +00:00Commented Sep 7, 2017 at 12:47 -
1\$\begingroup\$ @old_timer
(a<<8)|b
is always wrong for any system whereint
is 16 bits.a
will get implicitly promoted toint
which is signed. In casea
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– Lundin2017年09月07日 13:38:52 +00:00Commented Sep 7, 2017 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– next-hack2017年09月07日 14:25:17 +00:00Commented Sep 7, 2017 at 14:25
-
1\$\begingroup\$ This would be a better answer if it specified the compiler and relevant options in use. \$\endgroup\$Russell Borogove– Russell Borogove2017年09月07日 15:56:59 +00:00Commented Sep 7, 2017 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– user2017年09月08日 11:15:51 +00:00Commented Sep 8, 2017 at 11:15
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.
-
5\$\begingroup\$ "the compiler is required to promote them to unsigned int". No, the compiler is required to promote them to
int
, sincechar
will most likely not have the same conversion rank asint
on any platform. \$\endgroup\$Lundin– Lundin2017年09月07日 12:33:55 +00:00Commented Sep 7, 2017 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.2Each 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– Lundin2017年09月07日 12:54:27 +00:00Commented Sep 7, 2017 at 12:54