3.5.2 How Can I Make My Code Smaller?

There are a number of ways that this can be done, but results vary from one project to the next. Use the assembly list file (see 6.3 Assembly List Files) to observe the assembly code, produced by the compiler, to verify that the following tips are relevant to your code.

Use the smallest data types possible, as less code is needed to access these (this also reduces RAM usage). Note that a bit type and non-standard 24-bit integer type exists for this compiler. Avoid multi-bit bit-fields whenever possible. The code used to access these can be very large. See 5.3 Supported Data Types and Variables for all data types and sizes.

There are two sizes of floating-point type when using C90, with these being discussed in the same section as well. Avoid floating-point if at all possible. Consider writing fixed-point arithmetic code.

Use unsigned types, if possible, instead of signed types; particularly if they are used in expressions with a mix of types and sizes. Try to avoid an operator acting on operands with mixed sizes whenever possible.

Whenever you have a loop or condition code, use a “strong” stop condition, for example, the following:
for(i=0; i!=10; i++)
is preferable to:
for(i=0; i<10; i++)

A check for equality (== or !=) is usually more efficient to implement than the weaker < comparison.

In some situations, using a loop counter that decrements to zero is more efficient than one that starts at zero and counts up by the same number of iterations. This is more likely to be the case if the loop index is a byte-wide type. So you might be able to rewrite the above as:
for(i=10; i!=0; i--)
There might be a small advantage in changing the order of function parameters so that the first parameter is byte sized. A register is used if the first parameter is byte-sized. For example consider:
char calc(char mode, int value);
over
char calc(int value, char mode);
If you access structure members via a pointer, place most-accessed member first in the structure definition, for example:
struct MYSTRUCT {
    int popular;    // access to this member occurs throughout the program 
    int avoided;    // the following members can be placed in any order
    int unliked;
} myStruct;

struct MYSTRUCT * stp = &myStruct;
Indirectly accessing structure members requires a runtime addition of the structure's base address and member offset; however, when accessing the first member, that offset is zero, and so the addition is not required. This makes the access code smaller and faster.
If you define multi-dimensional arrays, consideration of the array dimensions might improve the code to access the array elements. A two-dimensional array for example can be considered as an array of subarrays. If an array element is accessed using indices that are variables, this access requires a runtime multiplication by a value being the number of elements in the second dimension. For example, if you define the array:
char myArray[2][5];
and access an element using the code myArray[x][y], this performs an operation equivalent to the C expression:
*(&myArray + y + x * 5)
This involves a multiplication by 5 (the number of elements in the second dimension). If your target device does not implement a hardware multiply instruction, this multiplication might involve a call to a library routine. If the array had been instead defined with the dimensions swapped:
char myArray[5][2];
then accessing the myArray[x][y] element would be equivalent to the C expression:
*(&myArray + y + x * 2)
which involves a more-friendly multiply by two. This is often performed with a simple shift.

In the same way, accessing an element of a single-dimension array of structures will require multiplication by the structure size. If for example you had an array of structures and each structure was 3 bytes long, adding an extra byte to the structure would increase the size of the memory required to store the array, but accessing elements that were 4 bytes wide might be faster than accessing elements that were 3 bytes wide.

Ensure that all optimizations are enabled (see 4.6.6 Options for Controlling Optimization). Be aware of what optimizations the compiler performs (see 5.13 Optimizations and 6.2 Assembly-Level Optimizations) so you can take advantage of them and don’t waste your time manually performing optimizations in C code that the compiler already handles, e.g., don’t turn a multiply-by-4 operation into a shift-by-2 operation as this sort of optimization is already detected.