Yes, using an int instead of a char will often result in a noticeable performance freebie. Thus the use of int by the C language to try to match the native size of the registers in the processor.
It is a good idea to use unsigned ints wherever possible, and only on the rare/specific occasion, use something other than an unsigned int. Avoid using something smaller than an int unless you have a really good reason. If you are trying to use smaller than an int items to get some freebie performance from a programming habit you need to change your habit the other way. Same goes for unsigned, use unsigned anything unless you have a really good reason to use signed.
Basically disassemble (or compile to asm), look at what your favorite and other compilers are generating, notice the unaligned addressing caused by chars, note the masking of the upper bits, the sign extension for signed chars, etc. These are sometimes free and sometimes not depending on where that byte is coming from and going to and the platform. Also try at a minimum x86 and arm, perhaps mips, gcc 3.x, 4.x and llvm. In particular notice how a single char mixed in with a list of ints in a line of declarations may cause the ints that follow to not be aligned, which is fine for x86 from an address standpoint but will cost in performance (on an x86 even with a cache). Put your aligned variables first then unaligned last. Other platforms that cannot or prefer not to do unaligned accesses will waste the extra bytes as padding, so you are not necessarily saving memory. The premature optimization is trying to tune to the variable length. Use simple habits like use unsigned ints for everything unless you have a specific reason, put your larger, aligned, variables and structures first in a list of declarations, and the unaligned stuff last (shorts then chars).
Multiplies (and divides) make this habit ugly, avoiding multiplies and divides in code is the best habit to have. If you have to use one be quite knowledgeable about its implementation. It is much better to multiply two chars instead of two ints for example (if the numbers support it), so if you do happen to know the ints are really 7 bit or 5 bit or whatever quantities, typecast them down for the multiply and allow a hardware multiply to happen instead of a soft multiply. (can be dormant bug if those variable sizes change!!). Even though many processors have a hardware multiply it is very rare that it can actually be used directly. Unless you help the compiler it has to make a library call to check for overflow among other things, and may end up doing a soft multiply as a result, very costly. Divides are bad because most processors do not include a divide. And if they do you may fall into the same trap. with multiply a N bit * N bit turns into 2*N bits of result which is where the multiply problem comes in. With divide the numbers stay the same or get smaller. In both cases the isas dont always provide enough bits to cover the overflow and a library call is required to work around the processors hardware limitations.
Floating point is a similar story, just be careful with floating point. Dont use it unless absolutely necessary. Most folks dont remember off hand that
float a;
float b;
...
b = a * 1.0;
C assumes double precision unless otherwise specified, so the above multiply requires a to be converted to double, then multiplied then the result converted back to single. Some fpus can do the precision conversion in the same instruction at the cost of clocks, some cannot. Precision conversion is where the majority of your floating point processor errors live (or did). So either use doubles for everything, or be careful with your coding to avoid these pitfalls:
float a;
float b;
...
b = a * 1.0F;
Also most isas do not have an FPU so avoid floating point math even more than you avoid fixed point multiplies and divides. Assume most fpus have bugs. It is difficult to write good floating point code (the programmer often throws away a fair amount of the precision by just not knowing how to use it and write code for it).
A few simple habits and your code runs noticeably faster and cleaner as a freebie. Also the compiler doesnt have to work as hard so you fall into fewer compiler bugs.
EDIT adding a floating point precision example:
float fun1 ( float a )
{
return(a*7.1);
}
float fun2 ( float a )
{
return(a*7.1F);
}
the first function contained:
mulsd .LC0(%rip), %xmm0
using a 64 bit floating point constant
.LC0
.long 1717986918
.long 1075603046
and the second function contains the desired single precision multiply
mulss .LC1(%rip), %xmm0
with a single precision constant
.LC1
.long 1088631603
char fun1 ( char a )
{
return(a+7);
}
int fun2 ( int a )
{
return(a+7);
}
fun1:
add r0, r0, #7
and r0, r0, #255
bx lr
fun2:
add r0, r0, #7
bx lr